From 2f0c69b7e8acac1852a6e71f843ee113d8a517b0 Mon Sep 17 00:00:00 2001 From: ghiscoding <gbeaulac@gmail.com> Date: Sat, 18 Jan 2025 17:35:20 -0500 Subject: [PATCH 1/3] feat(vue): add rowspan to Slickgrid-Vue - also move/rename Example 43 as Example 17 since the Remote Example is long gone --- demos/vue/src/components/Example17.vue | 157 +++++ demos/vue/src/components/Example42.vue | 2 +- demos/vue/src/components/Example43.vue | 570 +++++++++++++++--- demos/vue/src/components/Example44.vue | 348 +++++++++++ demos/vue/src/router/index.ts | 6 +- demos/vue/test/cypress/e2e/example17.cy.ts | 83 +++ demos/vue/test/cypress/e2e/example43.cy.ts | 439 ++++++++++++-- demos/vue/test/cypress/e2e/example44.cy.ts | 458 ++++++++++++++ .../column-row-spanning.md | 6 + .../src/examples/example33.ts | 8 - .../column-row-spanning.md | 6 + .../src/components/SlickgridVue.vue | 9 +- .../src/interfaces/itemMetadata.interface.ts | 4 +- .../e2e/{example32.cy.ts => example43.cy.ts} | 0 .../e2e/{example33.cy.ts => example44.cy.ts} | 0 15 files changed, 1925 insertions(+), 171 deletions(-) create mode 100644 demos/vue/src/components/Example17.vue create mode 100644 demos/vue/src/components/Example44.vue create mode 100644 demos/vue/test/cypress/e2e/example17.cy.ts create mode 100644 demos/vue/test/cypress/e2e/example44.cy.ts rename test/cypress/e2e/{example32.cy.ts => example43.cy.ts} (100%) rename test/cypress/e2e/{example33.cy.ts => example44.cy.ts} (100%) diff --git a/demos/vue/src/components/Example17.vue b/demos/vue/src/components/Example17.vue new file mode 100644 index 000000000..8007d4958 --- /dev/null +++ b/demos/vue/src/components/Example17.vue @@ -0,0 +1,157 @@ +<script setup lang="ts"> +import { ExcelExportService } from '@slickgrid-universal/excel-export'; +import { type Column, type GridOption, SlickgridVue, toCamelCase } from 'slickgrid-vue'; +import { ref } from 'vue'; + +const gridCreated = ref(false); +const gridOptions = ref<GridOption>(); +const columnDefinitions = ref<Column[]>([]); +const dataset = ref<any[]>([]); +const templateUrl = ref(new URL('./data/users.csv', import.meta.url).href); +const uploadFileRef = ref(''); +const showSubTitle = ref(true); + +function disposeGrid() { + gridCreated.value = false; +} + +function handleFileImport(event: any) { + const file: File = event.target.files[0]; + if (file.name.endsWith('.csv')) { + const reader = new FileReader(); + reader.onload = (e: any) => { + const content = e.target.result; + dynamicallyCreateGrid(content); + }; + reader.readAsText(file); + } else { + alert('File must be a CSV file'); + } +} + +function handleDefaultCsv() { + const staticDataCsv = `First Name,Last Name,Age,Type\nBob,Smith,33,Teacher\nJohn,Doe,20,Student\nJane,Doe,21,Student`; + dynamicallyCreateGrid(staticDataCsv); + uploadFileRef.value = ''; +} + +function dynamicallyCreateGrid(csvContent: string) { + // dispose of any previous grid before creating a new one + gridCreated.value = false; + + const dataRows = csvContent?.split('\n'); + const colDefs: Column[] = []; + const outputData: any[] = []; + + // create column definitions + dataRows.forEach((dataRow, rowIndex) => { + const cellValues = dataRow.split(','); + const dataEntryObj: any = {}; + + if (rowIndex === 0) { + // the 1st row is considered to be the header titles, we can create the column definitions from it + for (const cellVal of cellValues) { + const camelFieldName = toCamelCase(cellVal); + colDefs.push({ + id: camelFieldName, + name: cellVal, + field: camelFieldName, + filterable: true, + sortable: true, + }); + } + } else { + // at this point all column defs were created and we can loop through them and + // we can now start adding data as an object and then simply push it to the dataset array + cellValues.forEach((cellVal, colIndex) => { + dataEntryObj[colDefs[colIndex].id] = cellVal; + }); + + // a unique "id" must be provided, if not found then use the row index and push it to the dataset + if ('id' in dataEntryObj) { + outputData.push(dataEntryObj); + } else { + outputData.push({ ...dataEntryObj, id: rowIndex }); + } + } + }); + + gridOptions.value = { + gridHeight: 300, + gridWidth: 800, + enableFiltering: true, + enableExcelExport: true, + externalResources: [new ExcelExportService()], + headerRowHeight: 35, + rowHeight: 33, + }; + + dataset.value = outputData; + columnDefinitions.value = colDefs; + gridCreated.value = true; +} + +function toggleSubTitle() { + showSubTitle.value = !showSubTitle.value; + const action = showSubTitle.value ? 'remove' : 'add'; + document.querySelector('.subtitle')?.classList[action]('hidden'); +} +</script> + +<template> + <h2> + Example 17: Dynamically Create Grid from CSV / Excel import + <span class="float-end"> + <a + style="font-size: 18px" + target="_blank" + href="https://github.com/ghiscoding/slickgrid-universal/blob/master/demos/vue/src/components/Example17.vue" + > + <span class="mdi mdi-link-variant"></span> code + </a> + </span> + <button class="ms-2 btn btn-outline-secondary btn-sm btn-icon" type="button" data-test="toggle-subtitle" @click="toggleSubTitle()"> + <span class="mdi mdi-information-outline" title="Toggle example sub-title details"></span> + </button> + </h2> + + <div class="subtitle"> + Allow creating a grid dynamically by importing an external CSV or Excel file. This script demo will read the CSV file and will consider + the first row as the column header and create the column definitions accordingly, while the next few rows will be considered the + dataset. Note that this example is demoing a CSV file import but in your application you could easily implemnt an Excel file uploading. + </div> + + <div>A default CSV file can be download <a id="template-dl" :href="templateUrl">here</a>.</div> + + <div class="d-flex mt-5 align-items-end"> + <div class="file-upload"> + <label for="formFile" class="form-label">Choose a CSV file…</label> + <input class="form-control" type="file" data-test="file-upload-input" :value="uploadFileRef" @change="handleFileImport" /> + </div> + <span class="mx-3">or</span> + <div> + <button id="uploadBtn" data-test="static-data-btn" class="btn btn-outline-secondary" @click="handleDefaultCsv"> + Use default CSV data + </button> + / + <button class="btn btn-outline-danger btn-sm ms-2" @click="disposeGrid()">Destroy Grid</button> + </div> + </div> + + <hr /> + + <slickgrid-vue + v-if="gridCreated" + v-model:options="gridOptions" + v-model:columns="columnDefinitions as Column[]" + v-model:data="dataset" + grid-id="grid17" + > + </slickgrid-vue> +</template> + +<style lang="scss"> +.file-upload { + max-width: 300px; +} +</style> diff --git a/demos/vue/src/components/Example42.vue b/demos/vue/src/components/Example42.vue index 911194198..14480b9f9 100644 --- a/demos/vue/src/components/Example42.vue +++ b/demos/vue/src/components/Example42.vue @@ -142,7 +142,7 @@ function defineGrid() { }, enableExcelCopyBuffer: true, enableFiltering: true, - customPaginationComponent: CustomPagerComponent as DefineComponent<any, BasePaginationModel>, // load our Custom Pagination Component + customPaginationComponent: CustomPagerComponent as DefineComponent<any, BasePaginationModel, any>, // load our Custom Pagination Component enablePagination: true, pagination: { pageSize: pageSize.value, diff --git a/demos/vue/src/components/Example43.vue b/demos/vue/src/components/Example43.vue index d5c46a16c..4b8d1115d 100644 --- a/demos/vue/src/components/Example43.vue +++ b/demos/vue/src/components/Example43.vue @@ -1,94 +1,400 @@ <script setup lang="ts"> import { ExcelExportService } from '@slickgrid-universal/excel-export'; -import { type Column, type GridOption, SlickgridVue, toCamelCase } from 'slickgrid-vue'; -import { ref } from 'vue'; +import { type Column, Editors, type GridOption, type ItemMetadata, SlickgridVue, type SlickgridVueInstance } from 'slickgrid-vue'; +import { onBeforeMount, ref } from 'vue'; -const gridCreated = ref(false); +const isEditable = ref(false); const gridOptions = ref<GridOption>(); const columnDefinitions = ref<Column[]>([]); const dataset = ref<any[]>([]); -const templateUrl = ref(new URL('./data/users.csv', import.meta.url).href); -const uploadFileRef = ref(''); const showSubTitle = ref(true); +let vueGrid!: SlickgridVueInstance; +const metadata: ItemMetadata | Record<number, ItemMetadata> = { + // 10001: Davolio + 0: { + columns: { + 1: { rowspan: 2 }, + 2: { colspan: 2 }, + 6: { colspan: 3 }, + 10: { colspan: 3, rowspan: 10 }, + 13: { colspan: 2 }, + 17: { colspan: 2, rowspan: 2 }, + }, + }, + // 10002: (Buchanan... name not shown since Davolio has rowspan=2) + 1: { + columns: { + 3: { colspan: 3 }, + 6: { colspan: 4 }, + 13: { colspan: 2, rowspan: 5 }, + 15: { colspan: 2 }, + }, + }, + // 10003: Fuller + 2: { + columns: { + 2: { colspan: 3, rowspan: 2 }, + 5: { colspan: 2 }, + 7: { colspan: 3 }, + 15: { colspan: 2 }, + 17: { colspan: 2 }, + }, + }, + // 10004: Leverling + 3: { + columns: { + 6: { colspan: 4 }, + 16: { colspan: 2 }, + }, + }, + // 10005: Peacock + 4: { + columns: { + 2: { colspan: 4 }, + 7: { colspan: 3 }, + 15: { colspan: 2, rowspan: 2 }, + 17: { colspan: 2 }, + }, + }, + // 10006: Janet + 5: { + columns: { + 2: { colspan: 2 }, + 4: { colspan: 3 }, + 7: { colspan: 3 }, + 17: { colspan: 2 }, + }, + }, + // 10007: Suyama + 6: { + columns: { + 2: { colspan: 2 }, + 5: { colspan: 2 }, + 7: { colspan: 3 }, + 14: { colspan: 2 }, + 16: { colspan: 3 }, + }, + }, + // 10008: Robert + 7: { + columns: { + 2: { colspan: 3 }, + 5: { colspan: 3 }, + 13: { colspan: 3, rowspan: 2 }, + 16: { colspan: 2 }, + }, + }, + // 10009: Andrew + 8: { + columns: { + 2: { colspan: 3 }, + 7: { colspan: 3, rowspan: 2 }, + 17: { colspan: 2 }, + }, + }, + // 10010: Michael + 9: { + columns: { + 2: { colspan: 3 }, + 5: { colspan: 2 }, + 13: { colspan: 3 }, + 16: { colspan: 3 }, + }, + }, +}; -function disposeGrid() { - gridCreated.value = false; +onBeforeMount(() => { + defineGrid(); + // mock some data (different in each dataset) + dataset.value = loadData(); +}); + +/* Define grid Options and Columns */ +function defineGrid() { + columnDefinitions.value = [ + { id: 'employeeID', name: 'Employee ID', field: 'employeeID', minWidth: 100 }, + { id: 'employeeName', name: 'Employee Name', field: 'employeeName', editor: { model: Editors.text }, minWidth: 120 }, + { id: '9:00', name: '9:00 AM', field: '9:00', editor: { model: Editors.text }, minWidth: 120 }, + { id: '9:30', name: '9:30 AM', field: '9:30', editor: { model: Editors.text }, minWidth: 120 }, + { id: '10:00', name: '10:00 AM', field: '10:00', editor: { model: Editors.text }, minWidth: 120 }, + { id: '10:30', name: '10:30 AM', field: '10:30', editor: { model: Editors.text }, minWidth: 120 }, + { id: '11:00', name: '11:00 AM', field: '11:00', editor: { model: Editors.text }, minWidth: 120 }, + { id: '11:30', name: '11:30 AM', field: '11:30', editor: { model: Editors.text }, minWidth: 120 }, + { id: '12:00', name: '12:00 PM', field: '12:00', editor: { model: Editors.text }, minWidth: 120 }, + { id: '12:30', name: '12:30 PM', field: '12:30', editor: { model: Editors.text }, minWidth: 120 }, + { id: '1:00', name: '1:00 PM', field: '1:00', editor: { model: Editors.text }, minWidth: 120 }, + { id: '1:30', name: '1:30 PM', field: '1:30', editor: { model: Editors.text }, minWidth: 120 }, + { id: '2:00', name: '2:00 PM', field: '2:00', editor: { model: Editors.text }, minWidth: 120 }, + { id: '2:30', name: '2:30 PM', field: '2:30', editor: { model: Editors.text }, minWidth: 120 }, + { id: '3:00', name: '3:00 PM', field: '3:00', editor: { model: Editors.text }, minWidth: 120 }, + { id: '3:30', name: '3:30 PM', field: '3:30', editor: { model: Editors.text }, minWidth: 120 }, + { id: '4:00', name: '4:00 PM', field: '4:00', editor: { model: Editors.text }, minWidth: 120 }, + { id: '4:30', name: '4:30 PM', field: '4:30', editor: { model: Editors.text }, minWidth: 120 }, + { id: '5:00', name: '5:00 PM', field: '5:00', editor: { model: Editors.text }, minWidth: 120 }, + ]; + + gridOptions.value = { + autoResize: { + bottomPadding: 30, + rightPadding: 50, + }, + enableCellNavigation: true, + enableColumnReorder: true, + enableCellRowSpan: true, + enableExcelExport: true, + externalResources: [new ExcelExportService()], + enableExcelCopyBuffer: true, + autoEdit: true, + editable: false, + datasetIdPropertyName: 'employeeID', + frozenColumn: 0, + gridHeight: 348, + rowHeight: 30, + dataView: { + globalItemMetadataProvider: { + getRowMetadata: (_item, row) => { + return (metadata as Record<number, ItemMetadata>)[row]; + }, + }, + }, + rowTopOffsetRenderType: 'top', // rowspan doesn't render well with 'transform', default is 'top' + }; } -function handleFileImport(event: any) { - const file: File = event.target.files[0]; - if (file.name.endsWith('.csv')) { - const reader = new FileReader(); - reader.onload = (e: any) => { - const content = e.target.result; - dynamicallyCreateGrid(content); - }; - reader.readAsText(file); - } else { - alert('File must be a CSV file'); - } +function navigateDown() { + vueGrid?.slickGrid?.navigateDown(); } -function handleDefaultCsv() { - const staticDataCsv = `First Name,Last Name,Age,Type\nBob,Smith,33,Teacher\nJohn,Doe,20,Student\nJane,Doe,21,Student`; - dynamicallyCreateGrid(staticDataCsv); - uploadFileRef.value = ''; +function navigateUp() { + vueGrid?.slickGrid?.navigateUp(); } -function dynamicallyCreateGrid(csvContent: string) { - // dispose of any previous grid before creating a new one - gridCreated.value = false; - - const dataRows = csvContent?.split('\n'); - const colDefs: Column[] = []; - const outputData: any[] = []; - - // create column definitions - dataRows.forEach((dataRow, rowIndex) => { - const cellValues = dataRow.split(','); - const dataEntryObj: any = {}; - - if (rowIndex === 0) { - // the 1st row is considered to be the header titles, we can create the column definitions from it - for (const cellVal of cellValues) { - const camelFieldName = toCamelCase(cellVal); - colDefs.push({ - id: camelFieldName, - name: cellVal, - field: camelFieldName, - filterable: true, - sortable: true, - }); - } - } else { - // at this point all column defs were created and we can loop through them and - // we can now start adding data as an object and then simply push it to the dataset array - cellValues.forEach((cellVal, colIndex) => { - dataEntryObj[colDefs[colIndex].id] = cellVal; - }); - - // a unique "id" must be provided, if not found then use the row index and push it to the dataset - if ('id' in dataEntryObj) { - outputData.push(dataEntryObj); - } else { - outputData.push({ ...dataEntryObj, id: rowIndex }); - } - } - }); +function navigateNext() { + vueGrid?.slickGrid?.navigateNext(); +} - gridOptions.value = { - gridHeight: 300, - gridWidth: 800, - enableFiltering: true, - enableExcelExport: true, - externalResources: [new ExcelExportService()], - headerRowHeight: 35, - rowHeight: 33, - }; +function navigatePrev() { + vueGrid?.slickGrid?.navigatePrev(); +} - dataset.value = outputData; - columnDefinitions.value = colDefs; - gridCreated.value = true; +function toggleEditing() { + isEditable.value = !isEditable.value; + vueGrid.slickGrid.setOptions({ editable: isEditable.value }); +} + +function loadData() { + return [ + { + employeeID: 10001, + employeeName: 'Davolio', + '9:00': 'Analysis Tasks', + '9:30': 'Analysis Tasks', + '10:00': 'Team Meeting', + '10:30': 'Testing', + '11:00': 'Development', + '11:30': 'Development', + '12:00': 'Development', + '12:30': 'Support', + '1:00': 'Lunch Break', + '1:30': 'Lunch Break', + '2:00': 'Lunch Break', + '2:30': 'Testing', + '3:00': 'Testing', + '3:30': 'Development', + '4:00': 'Conference', + '4:30': 'Team Meeting', + '5:00': 'Team Meeting', + }, + { + employeeID: 10002, + employeeName: 'Buchanan', + '9:00': 'Task Assign', + '9:30': 'Support', + '10:00': 'Support', + '10:30': 'Support', + '11:00': 'Testing', + '11:30': 'Testing', + '12:00': 'Testing', + '12:30': 'Testing', + '1:00': 'Lunch Break', + '1:30': 'Lunch Break', + '2:00': 'Lunch Break', + '2:30': 'Development', + '3:00': 'Development', + '3:30': 'Check Mail', + '4:00': 'Check Mail', + '4:30': 'Team Meeting', + '5:00': 'Team Meeting', + }, + { + employeeID: 10003, + employeeName: 'Fuller', + '9:00': 'Check Mail', + '9:30': 'Check Mail', + '10:00': 'Check Mail', + '10:30': 'Analysis Tasks', + '11:00': 'Analysis Tasks', + '11:30': 'Support', + '12:00': 'Support', + '12:30': 'Support', + '1:00': 'Lunch Break', + '1:30': 'Lunch Break', + '2:00': 'Lunch Break', + '2:30': 'Development', + '3:00': 'Development', + '3:30': 'Team Meeting', + '4:00': 'Team Meeting', + '4:30': 'Development', + '5:00': 'Development', + }, + { + employeeID: 10004, + employeeName: 'Leverling', + '9:00': 'Testing', + '9:30': 'Check Mail', + '10:00': 'Check Mail', + '10:30': 'Support', + '11:00': 'Testing', + '11:30': 'Testing', + '12:00': 'Testing', + '12:30': 'Testing', + '1:00': 'Lunch Break', + '1:30': 'Lunch Break', + '2:00': 'Lunch Break', + '2:30': 'Development', + '3:00': 'Development', + '3:30': 'Check Mail', + '4:00': 'Conference', + '4:30': 'Conference', + '5:00': 'Team Meeting', + }, + { + employeeID: 10005, + employeeName: 'Peacock', + '9:00': 'Task Assign', + '9:30': 'Task Assign', + '10:00': 'Task Assign', + '10:30': 'Task Assign', + '11:00': 'Check Mail', + '11:30': 'Support', + '12:00': 'Support', + '12:30': 'Support', + '1:00': 'Lunch Break', + '1:30': 'Lunch Break', + '2:00': 'Lunch Break', + '2:30': 'Development', + '3:00': 'Development', + '3:30': 'Team Meeting', + '4:00': 'Team Meeting', + '4:30': 'Testing', + '5:00': 'Testing', + }, + { + employeeID: 10006, + employeeName: 'Janet', + '9:00': 'Testing', + '9:30': 'Testing', + '10:00': 'Support', + '10:30': 'Support', + '11:00': 'Support', + '11:30': 'Team Meeting', + '12:00': 'Team Meeting', + '12:30': 'Team Meeting', + '1:00': 'Lunch Break', + '1:30': 'Lunch Break', + '2:00': 'Lunch Break', + '2:30': 'Development', + '3:00': 'Development', + '3:30': 'Team Meeting', + '4:00': 'Team Meeting', + '4:30': 'Development', + '5:00': 'Development', + }, + { + employeeID: 10007, + employeeName: 'Suyama', + '9:00': 'Analysis Tasks', + '9:30': 'Analysis Tasks', + '10:00': 'Testing', + '10:30': 'Development', + '11:00': 'Development', + '11:30': 'Testing', + '12:00': 'Testing', + '12:30': 'Testing', + '1:00': 'Lunch Break', + '1:30': 'Lunch Break', + '2:00': 'Lunch Break', + '2:30': 'Support', + '3:00': 'Build', + '3:30': 'Build', + '4:00': 'Check Mail', + '4:30': 'Check Mail', + '5:00': 'Check Mail', + }, + { + employeeID: 10008, + employeeName: 'Robert', + '9:00': 'Task Assign', + '9:30': 'Task Assign', + '10:00': 'Task Assign', + '10:30': 'Development', + '11:00': 'Development', + '11:30': 'Development', + '12:00': 'Testing', + '12:30': 'Support', + '1:00': 'Lunch Break', + '1:30': 'Lunch Break', + '2:00': 'Lunch Break', + '2:30': 'Check Mail', + '3:00': 'Check Mail', + '3:30': 'Check Mail', + '4:00': 'Team Meeting', + '4:30': 'Team Meeting', + '5:00': 'Build', + }, + { + employeeID: 10009, + employeeName: 'Andrew', + '9:00': 'Check Mail', + '9:30': 'Team Meeting', + '10:00': 'Team Meeting', + '10:30': 'Support', + '11:00': 'Testing', + '11:30': 'Development', + '12:00': 'Development', + '12:30': 'Development', + '1:00': 'Lunch Break', + '1:30': 'Lunch Break', + '2:00': 'Lunch Break', + '2:30': 'Check Mail', + '3:00': 'Check Mail', + '3:30': 'Check Mail', + '4:00': 'Team Meeting', + '4:30': 'Development', + '5:00': 'Development', + }, + { + employeeID: 10010, + employeeName: 'Michael', + '9:00': 'Task Assign', + '9:30': 'Task Assign', + '10:00': 'Task Assign', + '10:30': 'Analysis Tasks', + '11:00': 'Analysis Tasks', + '11:30': 'Development', + '12:00': 'Development', + '12:30': 'Development', + '1:00': 'Lunch Break', + '1:30': 'Lunch Break', + '2:00': 'Lunch Break', + '2:30': 'Testing', + '3:00': 'Testing', + '3:30': 'Testing', + '4:00': 'Build', + '4:30': 'Build', + '5:00': 'Build', + }, + ]; } function toggleSubTitle() { @@ -96,11 +402,15 @@ function toggleSubTitle() { const action = showSubTitle.value ? 'remove' : 'add'; document.querySelector('.subtitle')?.classList[action]('hidden'); } + +function vueGridReady(grid: SlickgridVueInstance) { + vueGrid = grid; +} </script> <template> <h2> - Example 43: Dynamically Create Grid from CSV / Excel import + Example 43: colspan/rowspan - Employees Timesheets <span class="float-end"> <a style="font-size: 18px" @@ -116,42 +426,104 @@ function toggleSubTitle() { </h2> <div class="subtitle"> - Allow creating a grid dynamically by importing an external CSV or Excel file. This script demo will read the CSV file and will consider - the first row as the column header and create the column definitions accordingly, while the next few rows will be considered the - dataset. Note that this example is demoing a CSV file import but in your application you could easily implemnt an Excel file uploading. + <p class="italic example-details"> + <b>NOTES</b>: <code>rowspan</code> is an opt-in feature, because of its small perf hit (it needs to loop through all row metadatas to + map all rowspan), and requires the <code>enableCellRowSpan</code> grid option to be enabled to work properly. The + <code>colspan</code>/<code>rowspan</code> are both using DataView item metadata and are both based on row indexes and will + <b>not</b> keep the row in sync with the data. It is really up to you the user to update the metadata logic of how and where the cells + should span when a side effect kicks in. (i.e: Filtering/Sorting/Paging/Column Reorder... will <b>not</b> change/update the spanning + in the grid by itself and that is why they these features are all disabled in this example). Also, column/row freezing (pinning) are + also not supported, or at least not recommended unless you know exactly what you're doing (like in this demo here because we know our + pinning doesn't intersect)! Any freezing column/row that could intersect with a <code>colspan</code>/<code>rowspan</code> + <b>will cause problems</b>. + </p> </div> - <div>A default CSV file can be download <a id="template-dl" :href="templateUrl">here</a>.</div> - - <div class="d-flex mt-5 align-items-end"> - <div class="file-upload"> - <label for="formFile" class="form-label">Choose a CSV file…</label> - <input class="form-control" type="file" data-test="file-upload-input" :value="uploadFileRef" @change="handleFileImport" /> - </div> - <span class="mx-3">or</span> - <div> - <button id="uploadBtn" data-test="static-data-btn" class="btn btn-outline-secondary" @click="handleDefaultCsv"> - Use default CSV data - </button> - / - <button class="btn btn-outline-danger btn-sm ms-2" @click="disposeGrid()">Destroy Grid</button> - </div> - </div> + <button + class="ms-2 btn btn-outline-secondary btn-sm btn-icon" + data-test="goto-up" + @click="navigateUp()" + title="from an active cell, navigate to cell above" + > + <span class="mdi mdi-chevron-down mdi-rotate-180"></span> + Navigate Up Cell + </button> + <button + class="ms-2 btn btn-outline-secondary btn-sm btn-icon" + data-test="goto-down" + @click="navigateDown()" + title="from an active cell, navigate to cell below" + > + <span class="mdi mdi-chevron-down"></span> + Navigate Down Cell + </button> + <button + class="ms-2 btn btn-outline-secondary btn-sm btn-icon" + data-test="goto-prev" + @click="navigatePrev()" + title="from an active cell, navigate to previous left cell" + > + <span class="mdi mdi-chevron-down mdi-rotate-90"></span> + Navigate to Left Cell + </button> + <button + class="ms-2 btn btn-outline-secondary btn-sm btn-icon" + data-test="goto-next" + @click="navigateNext()" + title="from an active cell, navigate to next right cell" + > + <span class="mdi mdi-chevron-down mdi-rotate-270"></span> + Navigate to Right Cell + </button> + <button class="ms-2 btn btn-outline-secondary btn-sm btn-icon mx-1" @click="toggleEditing()" data-test="toggle-editing"> + <span class="mdi mdi-pencil-outline"></span> + <span + >Toggle Editing: <span id="isEditable" class="text-italic">{{ isEditable }}</span></span + > + </button> - <hr /> + <div class="grid-container grid43-container"> + <div class="grid43"></div> + </div> <slickgrid-vue - v-if="gridCreated" v-model:options="gridOptions" v-model:columns="columnDefinitions as Column[]" v-model:data="dataset" grid-id="grid43" + @onVueGridCreated="vueGridReady($event.detail)" > </slickgrid-vue> </template> <style lang="scss"> -.file-upload { - max-width: 300px; +#grid43 { + --slick-border-color: #d4d4d4; + // --slick-cell-border-left: 1px solid var(--slick-border-color); + --slick-header-menu-display: none; + --slick-header-column-height: 20px; + --slick-grid-border-color: #d4d4d4; + --slick-row-selected-color: #d4ebfd; + --slick-cell-active-box-shadow: inset 0 0 0 1px #3ca4ff; + + --slick-row-mouse-hover-box-shadow: 0; + --slick-cell-odd-background-color: #fff; + // --slick-cell-border-top: 0; + --slick-cell-border-right: 1px solid var(--slick-border-color); + --slick-cell-border-bottom: 1px solid var(--slick-border-color); + // --slick-cell-border-left: 1px; + --slick-cell-box-shadow: none; + --slick-row-mouse-hover-color: #fff; + --slick-cell-display: flex; + + .slick-cell.rowspan { + // background: white; + z-index: 9; + } + .slick-cell { + display: flex; + align-items: center; + /* justify-content: center; */ + } } </style> diff --git a/demos/vue/src/components/Example44.vue b/demos/vue/src/components/Example44.vue new file mode 100644 index 000000000..a56eb6a28 --- /dev/null +++ b/demos/vue/src/components/Example44.vue @@ -0,0 +1,348 @@ +<script setup lang="ts"> +import { type Column, type Formatter, type GridOption, type ItemMetadata, SlickgridVue, type SlickgridVueInstance } from 'slickgrid-vue'; +import { onBeforeMount, ref } from 'vue'; + +const gridOptions = ref<GridOption>(); +const columnDefinitions = ref<Column[]>([]); +let vueGrid!: SlickgridVueInstance; +const dataLn = ref<number | string>('loading...'); +const scrollToRow = ref(100); +const dataset = ref<any[]>([]); +const showSubTitle = ref(true); +const metadata: Record<number, ItemMetadata> = { + 0: { + columns: { + 1: { rowspan: 3 }, + }, + }, + 2: { + columns: { + 0: { rowspan: 3 }, + 3: { colspan: 3 }, + }, + }, + 3: { + columns: { + 1: { rowspan: 5, colspan: 1, cssClass: 'cell-var-span' }, + // 1: { rowspan: 3, colspan: 2, cssClass: "cell-var-span" }, + 3: { rowspan: 3, colspan: 5 }, + }, + }, + 8: { + columns: { + 1: { rowspan: 80 }, + 3: { rowspan: 1999, colspan: 2, cssClass: 'cell-very-high' }, + }, + }, + 12: { + columns: { + 11: { rowspan: 3 }, + }, + }, + 15: { + columns: { + 18: { colspan: 4, rowspan: 3 }, + }, + }, + 85: { + columns: { + 5: { rowspan: 20 }, + }, + }, +}; +const rowCellValueFormatter: Formatter = (row, cell, value) => { + return `<div class="cellValue">${value.toFixed(2)}</div><div class="valueComment">${row}.${cell}</div>`; +}; + +onBeforeMount(() => { + defineGrid(); + // mock some data (different in each dataset) + loadData(500); +}); + +/* Define grid Options and Columns */ +function defineGrid() { + columnDefinitions.value = [ + { id: 'title', name: 'Title', field: 'title', minWidth: 80 }, + { id: 'revenueGrowth', name: 'Revenue Growth', field: 'revenueGrowth', formatter: rowCellValueFormatter, minWidth: 120 }, + { + id: 'pricingPolicy', + name: 'Pricing Policy', + field: 'pricingPolicy', + minWidth: 110, + sortable: true, + formatter: rowCellValueFormatter, + }, + { id: 'policyIndex', name: 'Policy Index', field: 'policyIndex', minWidth: 100, formatter: rowCellValueFormatter }, + { id: 'expenseControl', name: 'Expense Control', field: 'expenseControl', minWidth: 110, formatter: rowCellValueFormatter }, + { id: 'excessCash', name: 'Excess Cash', field: 'excessCash', minWidth: 100, formatter: rowCellValueFormatter }, + { id: 'netTradeCycle', name: 'Net Trade Cycle', field: 'netTradeCycle', minWidth: 110, formatter: rowCellValueFormatter }, + { id: 'costCapital', name: 'Cost of Capital', field: 'costCapital', minWidth: 100, formatter: rowCellValueFormatter }, + { id: 'revenueGrowth2', name: 'Revenue Growth', field: 'revenueGrowth2', formatter: rowCellValueFormatter, minWidth: 120 }, + { + id: 'pricingPolicy2', + name: 'Pricing Policy', + field: 'pricingPolicy2', + minWidth: 110, + sortable: true, + formatter: rowCellValueFormatter, + }, + { id: 'policyIndex2', name: 'Policy Index', field: 'policyIndex2', minWidth: 100, formatter: rowCellValueFormatter }, + { id: 'expenseControl2', name: 'Expense Control', field: 'expenseControl2', minWidth: 110, formatter: rowCellValueFormatter }, + { id: 'excessCash2', name: 'Excess Cash', field: 'excessCash2', minWidth: 100, formatter: rowCellValueFormatter }, + { id: 'netTradeCycle2', name: 'Net Trade Cycle', field: 'netTradeCycle2', minWidth: 110, formatter: rowCellValueFormatter }, + { id: 'costCapital2', name: 'Cost of Capital', field: 'costCapital2', minWidth: 100, formatter: rowCellValueFormatter }, + { id: 'revenueGrowth3', name: 'Revenue Growth', field: 'revenueGrowth3', formatter: rowCellValueFormatter, minWidth: 120 }, + { + id: 'pricingPolicy3', + name: 'Pricing Policy', + field: 'pricingPolicy3', + minWidth: 110, + sortable: true, + formatter: rowCellValueFormatter, + }, + { id: 'policyIndex3', name: 'Policy Index', field: 'policyIndex3', minWidth: 100, formatter: rowCellValueFormatter }, + { id: 'expenseControl3', name: 'Expense Control', field: 'expenseControl3', minWidth: 110, formatter: rowCellValueFormatter }, + { id: 'excessCash3', name: 'Excess Cash', field: 'excessCash3', minWidth: 100, formatter: rowCellValueFormatter }, + { id: 'netTradeCycle3', name: 'Net Trade Cycle', field: 'netTradeCycle3', minWidth: 110, formatter: rowCellValueFormatter }, + { id: 'costCapital3', name: 'Cost of Capital', field: 'costCapital3', minWidth: 100, formatter: rowCellValueFormatter }, + ]; + + gridOptions.value = { + enableCellNavigation: true, + enableColumnReorder: true, + enableCellRowSpan: true, + gridHeight: 600, + gridWidth: 900, + rowHeight: 30, + dataView: { + globalItemMetadataProvider: { + getRowMetadata: (item: any, row: any) => renderDifferentColspan(item, row), + }, + }, + rowTopOffsetRenderType: 'top', // rowspan doesn't render well with 'transform', default is 'top' + }; +} + +function clearScrollTo() { + scrollToRow.value = 0; + document.querySelector<HTMLInputElement>('#nRow')?.focus(); +} + +function loadData(count: number) { + dataLn.value = 'loading...'; + + // add a delay just to show the "loading" text before it loads all data + setTimeout(() => { + // mock data + const tmpArray: any[] = []; + for (let i = 0; i < count; i++) { + tmpArray[i] = { + id: i, + title: 'Task ' + i, + revenueGrowth: Math.random() * Math.pow(10, Math.random() * 3), + pricingPolicy: Math.random() * Math.pow(10, Math.random() * 3), + policyIndex: Math.random() * Math.pow(10, Math.random() * 3), + expenseControl: Math.random() * Math.pow(10, Math.random() * 3), + excessCash: Math.random() * Math.pow(10, Math.random() * 3), + netTradeCycle: Math.random() * Math.pow(10, Math.random() * 3), + costCapital: Math.random() * Math.pow(10, Math.random() * 3), + revenueGrowth2: Math.random() * Math.pow(10, Math.random() * 3), + pricingPolicy2: Math.random() * Math.pow(10, Math.random() * 3), + policyIndex2: Math.random() * Math.pow(10, Math.random() * 3), + expenseControl2: Math.random() * Math.pow(10, Math.random() * 3), + excessCash2: Math.random() * Math.pow(10, Math.random() * 3), + netTradeCycle2: Math.random() * Math.pow(10, Math.random() * 3), + costCapital2: Math.random() * Math.pow(10, Math.random() * 3), + revenueGrowth3: Math.random() * Math.pow(10, Math.random() * 3), + pricingPolicy3: Math.random() * Math.pow(10, Math.random() * 3), + policyIndex3: Math.random() * Math.pow(10, Math.random() * 3), + expenseControl3: Math.random() * Math.pow(10, Math.random() * 3), + excessCash3: Math.random() * Math.pow(10, Math.random() * 3), + netTradeCycle3: Math.random() * Math.pow(10, Math.random() * 3), + costCapital3: Math.random() * Math.pow(10, Math.random() * 3), + }; + } + + // let's keep column 3-4 as the row spanning from row 8 until the end of the grid + metadata[8].columns![3].rowspan = tmpArray.length - 8; + + vueGrid?.dataView?.beginUpdate(); + vueGrid?.dataView?.setItems(tmpArray); + vueGrid?.dataView?.endUpdate(); + dataLn.value = count; + }, 20); +} + +/** + * A callback to render different row column span + * Your callback will always have the "item" argument which you can use to decide on the colspan + * Your return object must always be in the form of:: { columns: { [columnName]: { colspan: number|'*' } }} + */ +function renderDifferentColspan(_item: any, row: any): any { + return (metadata[row] as ItemMetadata)?.attributes + ? metadata[row] + : (metadata[row] = { attributes: { 'data-row': row }, ...metadata[row] }); +} + +function handleToggleSpans() { + const cell = metadata[3].columns![1]; + if (cell.colspan === 1) { + cell.rowspan = 3; + cell.colspan = 2; + } else { + cell.rowspan = 5; + cell.colspan = 1; + } + + // row index 3 can have a rowspan of up to 5 rows, so we need to invalidate from row 3 + 5 (-1 because of zero index) + // so: 3 + 5 - 1 => row indexes 3 to 7 + vueGrid.slickGrid?.invalidateRows([3, 4, 5, 6, 7]); + vueGrid.slickGrid?.render(); +} + +function handleScrollTo() { + // const args = event.detail && event.detail.args; + vueGrid.slickGrid?.scrollRowToTop(scrollToRow.value); + return false; +} + +function toggleSubTitle() { + showSubTitle.value = !showSubTitle.value; + const action = showSubTitle.value ? 'remove' : 'add'; + document.querySelector('.subtitle')?.classList[action]('hidden'); +} + +function vueGridReady(grid: SlickgridVueInstance) { + vueGrid = grid; +} +</script> + +<template> + <h2> + Example 44: colspan/rowspan with large dataset + <span class="float-end"> + <a + style="font-size: 18px" + target="_blank" + href="https://github.com/ghiscoding/slickgrid-universal/blob/master/demos/vue/src/components/Example44.vue" + > + <span class="mdi mdi-link-variant"></span> code + </a> + </span> + <button class="ms-2 btn btn-outline-secondary btn-sm btn-icon" type="button" data-test="toggle-subtitle" @click="toggleSubTitle()"> + <span class="mdi mdi-information-outline" title="Toggle example sub-title details"></span> + </button> + </h2> + + <div class="subtitle"> + <p class="italic example-details"> + This page demonstrates <code>colspan</code> & <code>rowspan</code> using DataView with item metadata. <b>Note</b>: + <code>colspan</code> & <code>rowspan</code> are rendered via row/cell indexes, any operations that could change these indexes (i.e. + Filtering/Sorting/Paging/Column Reorder) will require you to implement proper logic to recalculate these indexes (it becomes your + responsability). This demo does not show this because it is up to you to decide what to do when the span changes shape (i.e. you + default to 3 rowspan but you filter a row in the middle, how do you want to proceed?). + </p> + </div> + + <div class="row"> + <div class="col"> + <button class="ms-1 btn btn-outline-secondary btn-sm" data-test="add-500-rows-btn" @click="loadData(500)">500 rows</button> + <button class="ms-1 btn btn-outline-secondary btn-sm" data-test="add-5k-rows-btn" @click="loadData(5000)">5k rows</button> + <button class="ms-1 btn btn-outline-secondary btn-sm" data-test="add-50k-rows-btn" @click="loadData(50000)">50k rows</button> + <button class="mx-1 btn btn-outline-secondary btn-sm" data-test="add-50k-rows-btn" @click="loadData(500000)">500k rows</button> + <label>data length: </label><span id="dataLn" :textcontent="dataLn"></span> + <button + id="toggleSpans" + class="ms-1 btn btn-outline-secondary btn-sm btn-icon mx-1" + @click="handleToggleSpans()" + data-test="toggleSpans" + > + <span class="mdi mdi-flip-vertical"></span> + <span>Toggle blue cell colspan & rowspan</span> + </button> + <button id="scrollTo" class="ms-1 btn btn-outline-secondary btn-sm btn-icon" @click="handleScrollTo()" data-test="scrollToBtn"> + <span class="mdi mdi-arrow-down"></span> + <span>Scroll To Row</span> + </button> + </div> + <div class="col"> + <div class="input-group input-group-sm" style="width: 100px"> + <input + v-model="scrollToRow" + id="nRow" + type="text" + data-test="nbrows" + class="form-control search-string" + placeholder="search value" + autocomplete="off" + /> + <button class="btn btn-sm btn-outline-secondary d-flex align-items-center" data-test="clearScrollTo" @click="clearScrollTo()"> + <span class="icon mdi mdi-close-thick"></span> + </button> + </div> + </div> + </div> + + <slickgrid-vue + v-model:options="gridOptions" + v-model:columns="columnDefinitions as Column[]" + v-model:data="dataset" + grid-id="grid44" + @onVueGridCreated="vueGridReady($event.detail)" + > + </slickgrid-vue> +</template> + +<style lang="scss"> +#grid44 { + --slick-cell-active-box-shadow: inset 0 0 0 1px #e35ddc; + + .slick-row.even .slick-cell.cell-very-high { + background-color: #f0ffe0; + } + .slick-row.odd .slick-cell.cell-var-span { + background-color: #87ceeb; + } + .slick-row .slick-cell.rowspan { + background-color: #95b7a2; + z-index: 10; + } + .slick-row[data-row='3'] .slick-cell.l3.rowspan { + background-color: #95b7a2; + } + .slick-row[data-row='2'] .slick-cell.l3.r5 { + background-color: #ddfffc; + } + .slick-row[data-row='0'] .slick-cell.rowspan, + .slick-row[data-row='8'] .slick-cell.rowspan { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAQ0lEQVQYV2N8/fr1fwY84M6dOwyM+BSBFKioqOBWBFMAsgSrScgKsCpCV4ChCJsCFEW4FMAV4VMAVnT8+PH/IG/iAwDA1DlezHn8bwAAAABJRU5ErkJggg==); + } + .slick-row[data-row='8'] .slick-cell.rowspan:nth-child(4) { + background: #f0ffe0; + } + .slick-row[data-row='12'] .slick-cell.rowspan { + background: #bd8b8b; + } + .slick-row[data-row='15'] .slick-cell.rowspan { + background: #edc12e; + } + .slick-row[data-row='85'] .slick-cell.rowspan { + background: #8baebd; + } + .slick-cell.active { + /* use a different active cell color to make it a bit more obvious */ + box-shadow: inset 0 0 0 1px #e35ddc; + } + .cellValue { + float: right; + font-size: 14px; + } + .valueComment { + color: #7c8983; + font-size: 12px; + font-style: italic; + width: fit-content; + } +} +</style> diff --git a/demos/vue/src/router/index.ts b/demos/vue/src/router/index.ts index d64df5356..cdf01ebeb 100644 --- a/demos/vue/src/router/index.ts +++ b/demos/vue/src/router/index.ts @@ -17,6 +17,7 @@ import Example13 from '../components/Example13.vue'; import Example14 from '../components/Example14.vue'; import Example15 from '../components/Example15.vue'; import Example16 from '../components/Example16.vue'; +import Example17 from '../components/Example17.vue'; import Example18 from '../components/Example18.vue'; import Example19 from '../components/Example19.vue'; import Example20 from '../components/Example20.vue'; @@ -43,6 +44,7 @@ import Example40 from '../components/Example40.vue'; import Example41 from '../components/Example41.vue'; import Example42 from '../components/Example42.vue'; import Example43 from '../components/Example43.vue'; +import Example44 from '../components/Example44.vue'; import Home from '../Home.vue'; export const routes: RouteRecordRaw[] = [ @@ -64,6 +66,7 @@ export const routes: RouteRecordRaw[] = [ { path: '/example14', name: '14- Column Span & Header Grouping', component: Example14 }, { path: '/example15', name: '15- Grid State & Local Storage', component: Example15 }, { path: '/example16', name: '16- Row Move Plugin', component: Example16 }, + { path: '/example17', name: '17- Create Grid from CSV', component: Example17 }, { path: '/example18', name: '18- Draggable Grouping', component: Example18 }, { path: '/example19', name: '19- Row Detail View', component: Example19 }, { path: '/example20', name: '20- Pinned Columns / Rows', component: Example20 }, @@ -89,7 +92,8 @@ export const routes: RouteRecordRaw[] = [ { path: '/example40', name: '40- Infinite Scroll from JSON data', component: Example40 }, { path: '/example41', name: '41- Drag & Drop', component: Example41 }, { path: '/example42', name: '42- Custom Pagination', component: Example42 }, - { path: '/example43', name: '43- Create Grid from CSV', component: Example43 }, + { path: '/example43', name: '43- Colspan/Rowspan (timesheets)', component: Example43 }, + { path: '/example44', name: '44- Colspan/Rowspan (large data)', component: Example44 }, ]; export const router = createRouter({ diff --git a/demos/vue/test/cypress/e2e/example17.cy.ts b/demos/vue/test/cypress/e2e/example17.cy.ts new file mode 100644 index 000000000..619d7842f --- /dev/null +++ b/demos/vue/test/cypress/e2e/example17.cy.ts @@ -0,0 +1,83 @@ +describe('Example 43 - Dynamically Create Grid from CSV / Excel import', () => { + const defaultCsvTitles = ['First Name', 'Last Name', 'Age', 'Type']; + const GRID_ROW_HEIGHT = 33; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example43`); + cy.get('h2').should('contain', 'Example 43: Dynamically Create Grid from CSV / Excel import'); + }); + + it('should load default CSV file and expect default column titles', () => { + cy.get('[data-test="static-data-btn"]').click(); + + cy.get('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(defaultCsvTitles[index])); + }); + + it('should expect default data in the grid', () => { + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'John'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '20'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '21'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Student'); + }); + + it('should sort by "Age" and expect it to be sorted in ascending order', () => { + cy.get('.slick-header-columns .slick-header-column:nth(2)').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'John'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '20'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Student'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '21'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '33'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher'); + }); + + it('should click again the "Age" column and expect it to be sorted in descending order', () => { + cy.get('.slick-header-columns .slick-header-column:nth(2)').click(); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '21'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student'); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'John'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '20'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Student'); + }); + + it('should filter Smith as "Last Name" and expect only 1 row in the grid', () => { + cy.get('.slick-headerrow .slick-headerrow-column:nth(1) input').type('Smith'); + + cy.get('.slick-row').should('have.length', 1); + + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33'); + cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher'); + }); +}); diff --git a/demos/vue/test/cypress/e2e/example43.cy.ts b/demos/vue/test/cypress/e2e/example43.cy.ts index 619d7842f..727d20e6c 100644 --- a/demos/vue/test/cypress/e2e/example43.cy.ts +++ b/demos/vue/test/cypress/e2e/example43.cy.ts @@ -1,83 +1,406 @@ -describe('Example 43 - Dynamically Create Grid from CSV / Excel import', () => { - const defaultCsvTitles = ['First Name', 'Last Name', 'Age', 'Type']; - const GRID_ROW_HEIGHT = 33; +describe('Example 43 - colspan/rowspan - Employees Timesheets', { retries: 0 }, () => { + const GRID_ROW_HEIGHT = 30; + const fullTitles = [ + 'Employee ID', + 'Employee Name', + '9:00 AM', + '9:30 AM', + '10:00 AM', + '10:30 AM', + '11:00 AM', + '11:30 AM', + '12:00 PM', + '12:30 PM', + '1:00 PM', + '1:30 PM', + '2:00 PM', + '2:30 PM', + '3:00 PM', + '3:30 PM', + '4:00 PM', + '4:30 PM', + '5:00 PM', + ]; it('should display Example title', () => { cy.visit(`${Cypress.config('baseUrl')}/example43`); - cy.get('h2').should('contain', 'Example 43: Dynamically Create Grid from CSV / Excel import'); + cy.get('h2').should('contain', 'Example 43: colspan/rowspan - Employees Timesheets'); }); - it('should load default CSV file and expect default column titles', () => { - cy.get('[data-test="static-data-btn"]').click(); - + it('should have exact column titles', () => { cy.get('.slick-header-columns') .children() - .each(($child, index) => expect($child.text()).to.eq(defaultCsvTitles[index])); + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should expect 1st column to be frozen (frozen)', () => { + cy.get('.grid-canvas-left .slick-cell.frozen').should('have.length', 10); + cy.get('.grid-canvas-right .slick-cell:not(.frozen)').should('have.length.above', 60); }); - it('should expect default data in the grid', () => { - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher'); - - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'John'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '20'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student'); - - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '21'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Student'); + describe('Spanning', () => { + it('should expect "Davolio", "Check Mail", and "Development" to all have rowspan of 2 in morning hours', () => { + cy.get(`[data-row=0] > .slick-cell.l1.r1.rowspan`).should('contain', 'Davolio'); + cy.get(`[data-row=0] > .slick-cell.l1.r1.rowspan`).should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) + ); + + cy.get(`[data-row=2] > .slick-cell.l2.r4.rowspan`).should('contain', 'Check Mail'); + cy.get(`[data-row=2] > .slick-cell.l2.r4.rowspan`).should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) + ); + + cy.get(`[data-row=8] > .slick-cell.l7.r9.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=8] > .slick-cell.l7.r9.rowspan`).should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 2) + ); + }); + + it('should expect "Lunch Break" to span over 3 columns and over all rows', () => { + cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); + cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 10) + ); + }); + + it('should expect a large "Development" section that spans over multiple columns & rows in the afternoon', () => { + cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); + cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5) + ); + }); }); - it('should sort by "Age" and expect it to be sorted in ascending order', () => { - cy.get('.slick-header-columns .slick-header-column:nth(2)').click(); + describe('Basic Key Navigations', () => { + it('should start at Employee 10001, then type "End" key and expect to be in "Team Meeting" between 4:30-5:00pm', () => { + cy.get('[data-row=0] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l0.r0.active').should('contain', '10001'); + cy.get('@active_cell').type('{end}'); + cy.get('[data-row=0] > .slick-cell.l17.r18.active').should('contain', 'Team Meeting'); + }); + + it('should start at Employee 10002, then type "End" key and also expect to be in "Team Meeting" between 4:30-5:00pm', () => { + cy.get('[data-row=1] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=1] > .slick-cell.l0.r0.active').should('contain', '10002'); + cy.get('@active_cell').type('{end}'); + cy.get('[data-row=0] > .slick-cell.l17.r18.active').should('contain', 'Team Meeting'); + }); + + it('should start at Employee 10004, then type "ArrowRight" key twice and expect to be in "Check Mail" between 9:00-10:30am', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('@active_cell').type('{rightarrow}{rightarrow}'); + cy.get('[data-row=2] > .slick-cell.l2.r4.active').should('contain', 'Check Mail'); + }); + + it('should start at Employee 10004, then type "ArrowRight" key 4x times and expect to be in "Testing" between 11:00-1:00pm', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}'); + cy.get('[data-row=3] > .slick-cell.l6.r9.active').should('contain', 'Testing'); + }); + + it('should start at Employee 10004, then type "ArrowRight" key 5x times and expect to be in "Lunch Break"', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}'); + cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); + }); + + it('should start at Employee 10004, then type "ArrowRight" key 6x times and expect to be in "Development" between 2:30-3:30pm', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}{rightarrow}'); + cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); + }); + + // then rollback by going backward + it('should be on Employee 10004 row at previous "Development" cell, then type "ArrowLeft" key once and expect to be in "Lunch Break"', () => { + cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).as('active_cell').click(); + cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); + cy.get('@active_cell').type('{leftarrow}'); + cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); + }); + + it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key once and expect to be in "Conference" between 4:00-5:00pm', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('@active_cell').type('{end}'); + cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); + cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).type('{leftarrow}'); + cy.get(`[data-row=3] > .slick-cell.l16.r17.active`).should('contain', 'Conference'); + }); + + it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key 3x times and expect to be back to "Development" between 2:30-3:30pm', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('@active_cell').type('{end}'); + cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); + cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).type('{leftarrow}{leftarrow}{leftarrow}'); + cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); + }); + + it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key 4x times and expect to be back to "Lunch Break"', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('@active_cell').type('{end}'); + cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); + cy.get('@active_cell').type('{leftarrow}{leftarrow}{leftarrow}{leftarrow}'); + cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); + }); + + it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "ArrowLeft" key 5x times and expect to be back to "Testing" between 11:00-1:00pm', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('@active_cell').type('{end}'); + cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); + cy.get('@active_cell').type('{leftarrow}{leftarrow}{leftarrow}{leftarrow}{leftarrow}'); + cy.get(`[data-row=3] > .slick-cell.l6.r9.active`).should('contain', 'Testing'); + }); + + // going down + it('should start at 10am "Team Meeting, then type "ArrowDown" key once and expect to be in "Support" between 9:30-11:00am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('@active_cell').type('{downarrow}'); + cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support'); + }); + + it('should start at 10am "Team Meeting, then type "ArrowDown" key twice and expect to be in "Check Email" between 9:00-10:30am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('@active_cell').type('{downarrow}{downarrow}'); + cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); + }); + + it('should start at 10am "Team Meeting, then type "ArrowDown" key 3x times and expect to be in "Task Assign" between 9:00-11:00am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}'); + cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); + }); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'John'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '20'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Student'); + it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times and expect to be in "Support" between 10:00-11:30am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}'); + cy.get(`[data-row=5] > .slick-cell.l4.r6.active`).should('contain', 'Support'); + }); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '21'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student'); + // going up from inverse + it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" once and expect to be in "Task Assign" between 9:00-11:00am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}'); + cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); + }); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '33'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher'); + it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 2x times and expect to be in "Task Assign" between 9:00-11:00am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}{uparrow}'); + cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); + }); + + it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 3x times and expect to be in "Support" between 10:00-11:30am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}{uparrow}{uparrow}'); + cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support'); + }); + + it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 4x times and expect to be back to same "Team Meeting"', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('@active_cell').type('{downarrow}{downarrow}{downarrow}{downarrow}{uparrow}{uparrow}{uparrow}{uparrow}'); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + }); }); - it('should click again the "Age" column and expect it to be sorted in descending order', () => { - cy.get('.slick-header-columns .slick-header-column:nth(2)').click(); + describe('Grid Navigate Functions', () => { + it('should start at Employee 10004, then type "Navigate Right" twice and expect to be in "Check Mail" between 9:00-10:30am', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-test="goto-next"]').click().click(); + cy.get('[data-row=2] > .slick-cell.l2.r4.active').should('contain', 'Check Mail'); + }); + + it('should start at Employee 10004, then type "Navigate Right" 4x times and expect to be in "Testing" between 11:00-1:00pm', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-test="goto-next"]').click().click().click().click(); + cy.get('[data-row=3] > .slick-cell.l6.r9.active').should('contain', 'Testing'); + }); + + it('should start at Employee 10004, then type "Navigate Right" 5x times and expect to be in "Lunch Break"', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-test="goto-next"]').click().click().click().click().click(); + cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan`).should('contain', 'Lunch Break'); + }); + + it('should start at Employee 10004, then type "Navigate Right" 6x times and expect to be in "Development" between 2:30-3:30pm', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('[data-test="goto-next"]').click().click().click().click().click().click(); + cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); + }); + + // then rollback by going backward + it('should be on Employee 10004 row at previous "Development" cell, then type "Navigate Left" once and expect to be in "Lunch Break"', () => { + cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).as('active_cell').click(); + cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan`).should('contain', 'Development'); + cy.get('[data-test="goto-prev"]').click(); + cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); + }); + + it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" once and expect to be in "Conference" between 4:00-5:00pm', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('@active_cell').type('{end}'); + cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); + cy.get('[data-test="goto-prev"]').click(); + cy.get(`[data-row=3] > .slick-cell.l16.r17.active`).should('contain', 'Conference'); + }); + + it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" 3x times and expect to be back to "Development" between 2:30-3:30pm', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('@active_cell').type('{end}'); + cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).should('contain', 'Team Meeting'); + cy.get('[data-test="goto-prev"]').click().click().click(); + cy.get(`[data-row=1] > .slick-cell.l13.r14.rowspan.active`).should('contain', 'Development'); + }); + + it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" 4x times and expect to be back to "Lunch Break"', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('@active_cell').type('{end}'); + cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); + cy.get('[data-test="goto-prev"]').click().click().click().click(); + cy.get(`[data-row=0] > .slick-cell.l10.r12.rowspan.active`).should('contain', 'Lunch Break'); + }); + + it('should start at Employee 10004, type "End" and be at "Team Meeting" at 5pm, then type "Navigate Left" 5x times and expect to be back to "Testing" between 11:00-1:00pm', () => { + cy.get('[data-row=3] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=3] > .slick-cell.l0.r0.active').should('contain', '10004'); + cy.get('@active_cell').type('{end}'); + cy.get(`[data-row=3] > .slick-cell.l18.r18.active`).as('active_cell').should('contain', 'Team Meeting'); + cy.get('[data-test="goto-prev"]').click().click().click().click().click(); + cy.get(`[data-row=3] > .slick-cell.l6.r9.active`).should('contain', 'Testing'); + }); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher'); + // going down + it('should start at 10am "Team Meeting, then type "ArrowDown" key once and expect to be in "Support" between 9:30-11:00am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-test="goto-down"]').click(); + cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support'); + }); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '21'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student'); + it('should start at 10am "Team Meeting, then type "ArrowDown" key twice and expect to be in "Check Email" between 9:00-10:30am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-test="goto-down"]').click().click(); + cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); + }); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'John'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '20'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Student'); + it('should start at 10am "Team Meeting, then type "ArrowDown" key 3x times and expect to be in "Task Assign" between 9:00-11:00am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-test="goto-down"]').click().click().click(); + cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); + }); + + it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times and expect to be in "Support" between 10:00-11:30am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-test="goto-down"]').click().click().click().click(); + cy.get(`[data-row=5] > .slick-cell.l4.r6.active`).should('contain', 'Support'); + }); + + // going up from inverse + it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" once and expect to be in "Task Assign" between 9:00-11:00am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-test="goto-down"]').click().click().click().click(); + cy.get('[data-test="goto-up"]').click(); + cy.get(`[data-row=4] > .slick-cell.l2.r5.active`).should('contain', 'Task Assign'); + }); + + it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 2x times and expect to be in "Task Assign" between 9:00-11:00am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-test="goto-down"]').click().click().click().click(); + cy.get('[data-test="goto-up"]').click().click(); + cy.get(`[data-row=2] > .slick-cell.l2.r4.active`).should('contain', 'Check Mail'); + }); + + it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 3x times and expect to be in "Support" between 10:00-11:30am', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-test="goto-down"]').click().click().click().click(); + cy.get('[data-test="goto-up"]').click().click().click(); + cy.get(`[data-row=1] > .slick-cell.l3.r5.active`).should('contain', 'Support'); + }); + + it('should start at 10am "Team Meeting, then type "ArrowDown" key 4x times, then "ArrowUp" 4x times and expect to be back to same "Team Meeting"', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + cy.get('[data-test="goto-down"]').click().click().click().click(); + cy.get('[data-test="goto-up"]').click().click().click().click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('contain', 'Team Meeting'); + }); }); - it('should filter Smith as "Last Name" and expect only 1 row in the grid', () => { - cy.get('.slick-headerrow .slick-headerrow-column:nth(1) input').type('Smith'); + describe('Grid Editing', () => { + it('should toggle editing', () => { + cy.get('#isEditable').contains('false'); + cy.get('[data-row=0] > .slick-cell.l4.r4').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active .editor-text').should('not.exist'); + + cy.get('[data-test=toggle-editing]').click(); + cy.get('#isEditable').contains('true'); + + cy.get('[data-row=0] > .slick-cell.l4.r4').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active.editable .editor-text').should('exist'); + cy.get('[data-row=0] > .slick-cell.l4.r4.active.editable .editor-text').type('Team Meeting.xyz{enter}'); + }); + + // going down + it('should have changed active cell to "Support" between 9:30-11:00am', () => { + cy.get('[data-row=1] > .slick-cell.l3.r5.active.editable .editor-text') + .invoke('val') + .then((text) => expect(text).to.eq('Support')); + cy.get('[data-row=1] > .slick-cell.l3.r5.active.editable .editor-text').type('Support.xyz{enter}'); + }); + + it('should have changed active cell to "Check Email" between 9:00-10:30am', () => { + cy.get('[data-row=2] > .slick-cell.l2.r4.active.editable .editor-text') + .invoke('val') + .then((text) => expect(text).to.eq('Check Mail')); + cy.get('[data-row=2] > .slick-cell.l2.r4.active.editable .editor-text').type('Check Mail.xyz{enter}'); + }); + + it('should have changed active cell to "Task Assign" between 9:00-11:00am', () => { + cy.get('[data-row=4] > .slick-cell.l2.r5.active.editable .editor-text') + .invoke('val') + .then((text) => expect(text).to.eq('Task Assign')); + cy.get('[data-row=4] > .slick-cell.l2.r5.active.editable .editor-text').type('Task Assign.xyz{enter}'); + }); - cy.get('.slick-row').should('have.length', 1); + it('should have changed active cell to "Support" between 10:00-11:30am', () => { + cy.get('[data-row=5] > .slick-cell.l4.r6.active.editable .editor-text') + .invoke('val') + .then((text) => expect(text).to.eq('Support')); + cy.get('[data-row=5] > .slick-cell.l4.r6.active.editable .editor-text').type('Support.xyz{enter}'); + }); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33'); - cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher'); + it('should have changed active cell to "Testing" and cancel editing when typing "Escape" key', () => { + cy.get('[data-row=6] > .slick-cell.l4.r4.active.editable .editor-text') + .invoke('val') + .then((text) => expect(text).to.eq('Testing')); + cy.get('[data-row=6] > .slick-cell.l4.r4.active.editable .editor-text').type('{esc}'); + cy.get('[data-row=6] > .slick-cell.l4.r4.active.editable .editor-text').should('not.exist'); + }); }); }); diff --git a/demos/vue/test/cypress/e2e/example44.cy.ts b/demos/vue/test/cypress/e2e/example44.cy.ts new file mode 100644 index 000000000..d7b64aaec --- /dev/null +++ b/demos/vue/test/cypress/e2e/example44.cy.ts @@ -0,0 +1,458 @@ +describe('Example 44 - Column & Row Span', { retries: 0 }, () => { + const GRID_ROW_HEIGHT = 30; + const fullTitles = [ + 'Title', + 'Revenue Growth', + 'Pricing Policy', + 'Policy Index', + 'Expense Control', + 'Excess Cash', + 'Net Trade Cycle', + 'Cost of Capital', + 'Revenue Growth', + 'Pricing Policy', + 'Policy Index', + 'Expense Control', + 'Excess Cash', + 'Net Trade Cycle', + 'Cost of Capital', + 'Revenue Growth', + 'Pricing Policy', + 'Policy Index', + 'Expense Control', + 'Excess Cash', + 'Net Trade Cycle', + 'Cost of Capital', + ]; + + it('should display Example title', () => { + cy.visit(`${Cypress.config('baseUrl')}/example44`); + cy.get('h2').should('contain', 'Example 44: colspan/rowspan with large dataset'); + }); + + it('should have exact column titles', () => { + cy.get('.slick-header-columns') + .children() + .each(($child, index) => expect($child.text()).to.eq(fullTitles[index])); + }); + + it('should drag Title column to swap with 2nd column "Revenue Growth" in the grid and expect rowspan to stay at same position with Task 0 to spread instead', () => { + const expectedTitles = ['Revenue Growth', 'Title', 'Pricing Policy']; + + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.l0.r0.rowspan`).should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3) + ); + + cy.get('.slick-header-columns').children('.slick-header-column:nth(0)').contains('Title').drag('.slick-header-column:nth(1)'); + cy.get('.slick-header-column:nth(0)').contains('Revenue Growth'); + cy.get('.slick-header-column:nth(1)').contains('Title'); + + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l1.r1`).should('contain', 'Task 0'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.l1.r1`).should('not.exist'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l1.r1`).should('contain', 'Task 3'); + + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l1.r1.rowspan`).should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3) + ); + + cy.get('.slick-header-columns') + .children() + .each(($child, index) => { + if (index < expectedTitles.length) { + expect($child.text()).to.eq(expectedTitles[index]); + } + }); + }); + + it('should drag back Title column to reswap with 2nd column "Revenue Growth" in the grid and expect rowspan to stay at same position with Revenue Growth to now spread', () => { + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l1.r1.rowspan`).should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3) + ); + cy.get('.slick-header-columns').children('.slick-header-column:nth(0)').contains('Revenue Growth').drag('.slick-header-column:nth(1)'); + cy.get('.slick-header-column:nth(0)').contains('Title'); + cy.get('.slick-header-column:nth(1)').contains('Revenue Growth'); + + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 0'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 1'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 2'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l0.r0`).should('not.exist'); + + const expectedTitles = ['Title', 'Revenue Growth', 'Pricing Policy']; + cy.get('.slick-header-columns') + .children() + .each(($child, index) => { + if (index < expectedTitles.length) { + expect($child.text()).to.eq(expectedTitles[index]); + } + }); + }); + + describe('spanning', () => { + it('should expect first row to be regular rows without any spanning', () => { + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 0'); + + for (let i = 2; i <= 6; i++) { + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l${i}.r${i}`).should('exist'); + } + }); + + it('should expect 1st row, second cell to span (rowspan) across 3 rows', () => { + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 0'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1).rowspan`).should(($el) => { + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3); + }); + + for (let i = 2; i <= 14; i++) { + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(${i})`).contains(/\d+$/); // use regexp to make sure it's a number + } + }); + + it('should expect 3rd row first cell to span (rowspan) across 3 rows', () => { + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.l0.r0.rowspan`).should('contain', 'Task 2'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell.l0.r0.rowspan`).should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3) + ); + + for (let i = 2; i <= 5; i++) { + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(${i})`).contains(/\d+$/); + } + }); + + it('should expect 4th row to have 2 sections (blue, green) spanning across 3 rows (rowspan) and 2 columns (colspan)', () => { + // blue rowspan section + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l1.r1.rowspan`).should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5) + ); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l2.r2`) + .should('exist') + .contains(/\d+$/); + + // green colspan/rowspan section + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l3.r7`) + .should('exist') + .contains(/\d+$/); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l8.r8`) + .should('exist') + .contains(/\d+$/); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l9.r9`) + .should('exist') + .contains(/\d+$/); + }); + + it('should click on "Toggle blue cell colspan..." and expect colspan to widen from 1 column to 2 columns and from 5 rows to 3 rowspan', () => { + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l1.r1.rowspan`).should('exist'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l1.r1.rowspan`).should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 5) + ); + + cy.get('[data-test="toggleSpans"]').click(); + cy.get('.slick-cell.l1.r1.rowspan').should('exist'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell.l1.r2.rowspan`).should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3) + ); + }); + + it('should expect Task 8 on 2nd column to have rowspan spanning 80 cells', () => { + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 8'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell:nth(1).rowspan`).contains(/\d+$/); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell:nth(1).rowspan`).should(($el) => { + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 80); + }); + }); + + it('should scroll to the right and still expect spans without any extra texts', () => { + cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(400, 0).wait(10); + + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1)`).contains(/\d+$/); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(0).rowspan`).should('exist'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1).rowspan`).should('exist'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 3}px;"] > .slick-cell:nth(1).rowspan`).should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3) + ); + + // next rows are regular cells + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell.l3.r3`).should('not.exist'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 4}px;"] > .slick-cell.l4.r4`).should('not.exist'); + + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell.l3.r3`).should('not.exist'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 5}px;"] > .slick-cell.l3.r3`).should('not.exist'); + + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell.l4.r4`).should('exist'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 6}px;"] > .slick-cell.l4.r4`).should('exist'); + }); + + it('should scroll back to left and expect Task 8 to have 2 different spans (Revenue Grow: rowspan=80, Policy Index: rowspan=2000,colspan=2)', () => { + cy.get('.slick-viewport-top.slick-viewport-left').scrollTo(0, 0).wait(10); + + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 8'); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell:nth(1).rowspan`).should(($el) => { + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 80); + }); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell:nth(1)`).contains(/\d+$/); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell:nth(2)`).contains(/\d+$/); + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 8}px;"] > .slick-cell.l3.r4`).should('exist'); + + cy.get(`[style*="top: ${GRID_ROW_HEIGHT * 9}px;"] > .slick-cell.l0.r0`).should('contain', 'Task 9'); + }); + + it('should scroll to row 85 and still expect 3 spans in the screen, "Revenue Growth" and "Policy Index" spans', () => { + cy.get('[data-test="clearScrollTo"]').click(); + cy.get('[data-test="nbrows"]').type('{backspace}85'); + cy.get('[data-test="scrollToBtn"]').click(); + + // left dashed rowspan "Revenue Growth" + cy.get(`[data-row=85] > .slick-cell.l0.r0`).should('contain', 'Task 85'); + cy.get(`[data-row=85] > .slick-cell.l2.r2`).contains(/\d+$/); + + // rowspan middle (yellowish) "Policy Index" + cy.get(`[data-row=88] > .slick-cell.l0.r0`).should('contain', 'Task 88'); + cy.get(`[data-row=88] > .slick-cell.l1.r1`).should('exist'); + }); + + it('should scroll to the end of the grid and still expect "PolicyIndex" column to span across 2 columns and rows until the end of the grid', () => { + cy.get('[data-test="clearScrollTo"]').click(); + cy.get('[data-test="nbrows"]').type('{backspace}490'); + cy.get('[data-test="scrollToBtn"]').click(); + + cy.get(`[data-row=485] > .slick-cell.l0.r0`).should('contain', 'Task 485'); + + cy.get(`[data-row=499] > .slick-cell.l0.r0`).should('contain', 'Task 499'); + cy.get(`[data-row=499] > .slick-cell.l1.r1`).should('exist'); + cy.get(`[data-row=499] > .slick-cell.l2.r2`).should('exist'); + cy.get(`[data-row=499] > .slick-cell.l5.r5`).should('exist'); + }); + + it('should load 5K data and expect 8.3 rowspan row height to increase/decrease when data changes from 500 to 5K back to 500', () => { + cy.get('[data-row=8] .slick-cell.l3.r4').should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * (500 - 8)) + ); + + cy.get('[data-test="add-5k-rows-btn"]').click(); + cy.get('[data-row=8] .slick-cell.l3.r4').should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * (5000 - 8)) + ); + + cy.get('[data-test="add-500-rows-btn"]').click(); + cy.get('[data-row=8] .slick-cell.l3.r4').should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * (500 - 8)) + ); + }); + }); + + describe('Basic Key Navigations', () => { + it('should scroll back to top', () => { + cy.get('[data-test="clearScrollTo"]').click(); + cy.get('[data-test="nbrows"]').type('0'); + cy.get('[data-test="scrollToBtn"]').click(); + }); + + it('should start at Task 6 on PolicyIndex column, then type "Arrow Up" key and expect active cell to become the green section in the middle', () => { + cy.get('[data-row=6] > .slick-cell.l2.r2').as('active_cell').click(); + cy.get('[data-row=6] > .slick-cell.l2.r2.active').should('have.length', 1); + cy.get('@active_cell').type('{uparrow}'); + cy.get('[data-row=3] > .slick-cell.l1.r2.active').should('have.length', 1); + }); + + it('should start at Task 6 on PricingPolicy column, then type "Arrow Left" key and expect active cell to become the green section in the middle', () => { + cy.get('[data-row=6] > .slick-cell.l2.r2').as('active_cell').click(); + cy.get('[data-row=6] > .slick-cell.l2.r2.active').should('have.length', 1); + cy.get('[data-test="toggleSpans"]').click(); + cy.get('@active_cell').type('{leftarrow}'); + cy.get('[data-row=3] .slick-cell.l1.r1.active').should('have.length', 1); + }); + + it('should start at Task 5 on Task 5 column, then type "Arrow Right" key 3x times and expect active cell to become the wide green section in the middle', () => { + cy.get('[data-row=5] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=5] > .slick-cell.l0.r0.active').should('have.length', 1); + cy.get('@active_cell').type('{rightarrow}{rightarrow}'); + cy.get('[data-row=5] .slick-cell.l2.r2.active').should('have.length', 1); + cy.get('[data-row=5] .slick-cell.l2.r2.active').type('{rightarrow}'); + cy.get('[data-row=3] .slick-cell.l3.r7.active').should(($el) => + expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3) + ); + }); + + it('should start at Task 2 on PricingPolicy column, then type "Arrow Left" key and expect active cell to become the dashed section beside Task 0-3 on RevenueGrowth column', () => { + cy.get('[data-row=2] > .slick-cell.l2.r2').as('active_cell').click(); + cy.get('[data-row=2] > .slick-cell.l2.r2.active').should('have.length', 1); + cy.get('@active_cell').type('{leftarrow}'); + cy.get('[data-row=0] > .slick-cell.l1.r1.active').should('have.length', 1); + }); + + it('should start at Task 2 on PricingPolicy column, then type "Arrow Left" key twice and expect active cell to become Task 2 cell', () => { + cy.get('[data-row=2] > .slick-cell.l2.r2').as('active_cell').click(); + cy.get('[data-row=2] > .slick-cell.l2.r2.active').should('have.length', 1); + cy.get('@active_cell').type('{leftarrow}{leftarrow}'); + cy.get('[data-row=2] > .slick-cell.l0.r0.active').contains('Task 2'); + cy.get('[data-row=2] > .slick-cell.l0.r0.active').should('have.length', 1); + }); + + it('should start at Task 2 on PricingPolicy column, then type "Home" key and expect active cell to become Task 2 cell', () => { + cy.get('[data-row=2] > .slick-cell.l2.r2').as('active_cell').click(); + cy.get('[data-row=2] .slick-cell.l2.r2.active').should('have.length', 1); + cy.get('@active_cell').type('{home}'); + cy.get('[data-row=2] .slick-cell.l0.r0.active').contains('Task 2'); + cy.get('[data-row=2] .slick-cell.l0.r0.active').should('have.length', 1); + }); + + it('should start at Task 2 on PricingPolicy column, then type "End" key and expect active cell to become Task 2 cell', () => { + cy.get('[data-row=2] > .slick-cell.l2.r2').as('active_cell').click(); + cy.get('[data-row=2] .slick-cell.l2.r2.active').should('have.length', 1); + cy.get('@active_cell').type('{end}'); + cy.get('[data-row=2] .slick-cell.l21.r21.active').should('have.length', 1); + }); + + it('should start at RevenueGrowth column on first dashed cell, then type "Ctrl+End" then "Ctrl+Home" keys and expect active cell to go to bottom/top of grid on same column', () => { + cy.get('[data-row=0] > .slick-cell.l2.r2').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l2.r2.active').should('have.length', 1); + cy.get('@active_cell').type('{ctrl}{end}', { release: false }); + cy.get('[data-row=499] > .slick-cell.l21.r21.active').should('have.length', 1); + cy.get('[data-row=499] > .slick-cell.l21.r21.active').type('{ctrl}{home}', { release: false }); + cy.get('[data-row=0] .slick-cell.l0.r0.active').should('have.length', 1); + cy.get('[data-row=0] .slick-cell.l1.r1').should(($el) => expect(parseInt(`${$el.outerHeight()}`, 10)).to.eq(GRID_ROW_HEIGHT * 3)); + }); + + it('should start at first row on PolicyIndex column, then type "Ctrl+DownArrow" keys and expect active cell to become yellowish section', () => { + cy.get('[data-row=0] > .slick-cell.l3.r3').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l3.r3.active').should('have.length', 1); + cy.get('@active_cell').type('{ctrl}{downarrow}', { release: false }); + cy.get('[data-row=8] > .slick-cell.l3.r4.active').should('have.length', 1); + }); + + it('should start at first row on ExpenseControl column, then type "Ctrl+DownArrow" keys and expect active cell to become the cell just above the yellowish section', () => { + cy.get('[data-row=0] > .slick-cell.l4.r4').as('active_cell').click(); + cy.get('[data-row=0] > .slick-cell.l4.r4.active').should('have.length', 1); + cy.get('@active_cell').type('{ctrl}{downarrow}', { release: false }); + cy.get('[data-row=7] .slick-cell.l4.r4.active').should('have.length', 1); + }); + + it('should start at Task 13, type "End" key and expect active cell to be the last span cell', () => { + cy.get('[data-row=13] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=13] > .slick-cell.l0.r0.active').should('have.length', 1); + cy.get('@active_cell').type('{end}'); + cy.get('[data-row=13] > .slick-cell.l21.r21.active').should('have.length', 1); + }); + + it('should go to Task 13 last cell, type "Home" key and expect active cell to become Task 13', () => { + cy.get('[data-row=13] > .slick-cell.l21.r21').as('active_cell').click(); + cy.get('[data-row=13] > .slick-cell.l21.r21.active').should('have.length', 1); + cy.get('@active_cell').type('{home}'); + cy.get('[data-row=13] > .slick-cell.l0.r0.active').should('have.length', 1); + }); + + it('should start at Task 15, type "End" key and expect active cell to be on the last orange span cell', () => { + cy.get('[data-row=15] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=15] > .slick-cell.l0.r0.active').should('have.length', 1); + cy.get('@active_cell').type('{end}'); + cy.get('[data-row=15] > .slick-cell.l18.r21.active').should('have.length', 1); + cy.get('[data-row=15] > .slick-cell.l18.r21.active').type('{home}'); + }); + + it('should start at Task 17, type "End" key and expect active cell to be on the last orange span cell', () => { + cy.get('[data-row=17] > .slick-cell.l0.r0').as('active_cell').click(); + cy.get('[data-row=17] > .slick-cell.l0.r0.active').should('have.length', 1); + cy.get('@active_cell').type('{end}'); + cy.get('[data-row=15] > .slick-cell.l18.r21.active').should('have.length', 1); + cy.get('[data-row=15] > .slick-cell.l18.r21.active').type('{home}'); + }); + + it('should start at Task 5, type "ArrowRight" key 3x times and expect active cell to be at cell 3.3', () => { + cy.get('[data-row=15] > .slick-cell.l0.r0').type('{ctrl}{uparrow}'); + cy.get('[data-row=5] > .slick-cell.l0.r0').contains('Task 5').as('active_cell').click(); + cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}'); + cy.get('[data-row=3] > .slick-cell.l3.r7.active').should('have.length', 1); + }); + + it('should start at Task 5, type "ArrowRight" key 4x times and expect active cell to be at cell 5.8', () => { + cy.get('[data-row=5] > .slick-cell.l0.r0').contains('Task 5').as('active_cell').click(); + cy.get('[data-row=5] > .slick-cell.l0.r0.active').should('have.length', 1); + cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}'); + cy.get('[data-row=5] > .slick-cell.l8.r8.active').should('have.length', 1); + }); + + it('should start at Task 5, type "ArrowRight" key 4x times, then "ArrowLeft" 4x times and be back at Task 5', () => { + cy.get('[data-row=5] > .slick-cell.l0.r0').contains('Task 5').as('active_cell').click(); + cy.get('[data-row=5] > .slick-cell.l0.r0.active').should('have.length', 1); + cy.get('@active_cell').type('{rightarrow}{rightarrow}{rightarrow}{rightarrow}{leftarrow}{leftarrow}{leftarrow}{leftarrow}'); + cy.get('[data-row=5] > .slick-cell.l0.r0.active').should('have.length', 1); + }); + + it('should start at Task 2 on Pricing Policy column and type "PageDown" key 3x times and be on Task 59 on same column', () => { + cy.get('[data-row=2] > .slick-cell.l2.r2').as('active_cell').click(); + cy.get('@active_cell').type('{pagedown}{pagedown}{pagedown}'); + cy.get('[data-row=59] > .slick-cell.l2.r2.active').should('have.length', 1); + }); + + it('should start at Task 59 on Pricing Policy column and type "PageUp" key 3x times and be back to Task 2 on same column', () => { + cy.get('[data-row=59] > .slick-cell.l2.r2').as('active_cell').click(); + cy.get('@active_cell').type('{pageup}{pageup}{pageup}'); + cy.get('[data-row=2] > .slick-cell.l2.r2.active').should('have.length', 1); + }); + + it('should start at Task 0 on Policy Index column and type "PageDown" key 2x times but expect active cell to stay on initial cell but still scroll down around Task 40', () => { + cy.get('[data-row=0] > .slick-cell.l3.r3').as('active_cell').click(); + cy.get('@active_cell').type('{pagedown}{pagedown}'); + cy.get('[data-row=0] > .slick-cell.l3.r3.active').should('have.length', 1); + cy.get('[data-row=40]').should('be.visible'); + }); + + it('should start at Task 1 on Excess Cash column and type "PageDown" key 4x times and be on Task 77 on same column', () => { + cy.get('[data-row=0] > .slick-cell.l3.r3.active').type('{ctrl}{uparrow}'); + cy.get('[data-row=1] > .slick-cell.l5.r5').as('active_cell').click(); + cy.get('@active_cell').type('{pagedown}{pagedown}{pagedown}{pagedown}'); + cy.get('[data-row=77] > .slick-cell.l5.r5.active').should('have.length', 1); + }); + + it('should start at Task 77 on Excess Cash column and type "PageDown" key 4x times and be on Task 105 on same column', () => { + cy.get('[data-row=77] > .slick-cell.l5.r5').as('active_cell').click(); + cy.get('@active_cell').type('{pagedown}'); + cy.get('[data-row=105] > .slick-cell.l5.r5.active').should('have.length', 1); + }); + + it('should start at Task 105 on Excess Cash column and type "PageUp" key once and be on Task 85 on same column', () => { + cy.get('[data-row=105] > .slick-cell.l5.r5').as('active_cell').click(); + cy.get('@active_cell').type('{pageup}'); + cy.get('[data-row=85] > .slick-cell.l5.r5.active').should('have.length', 1); + }); + + it('should start at Task 85 on Excess Cash column and type "PageUp" key once and be on Task 66 on same column', () => { + cy.get('[data-row=85] > .slick-cell.l5.r5').as('active_cell').click(); + cy.get('@active_cell').type('{pageup}'); + cy.get('[data-row=66] > .slick-cell.l5.r5.active').should('have.length', 1); + }); + + it('should start at Task 66 on Excess Cash column and type "PageUp" key 3x times and be on Task 9 on same column', () => { + cy.get('[data-row=66] > .slick-cell.l5.r5').as('active_cell').click(); + cy.get('@active_cell').type('{pageup}{pageup}{pageup}'); + cy.get('[data-row=9] > .slick-cell.l5.r5.active').should('have.length', 1).type('{pageup}'); + }); + + it('should start at Task 0 on Revenue Growth column and type "PageDown" key once and be on Task 88 on same column', () => { + cy.get('[data-row=0] > .slick-cell.l1.r1').as('active_cell').click(); + cy.get('@active_cell').type('{pagedown}'); + cy.get('[data-row=88] > .slick-cell.l1.r1.active').should('have.length', 1); + }); + + it('should start at Task 88 on Revenue Growth column and type "PageUp" key once and be on Task 8 on same column', () => { + cy.get('[data-row=88] > .slick-cell.l1.r1').as('active_cell').click(); + cy.get('@active_cell').type('{pageup}'); + cy.get('[data-row=8] > .slick-cell.l1.r1.active').should('have.length', 1); + }); + + it('should start at Task 9 on Excess Cash column and type "PageUp" key once and be on Task 0 on same column', () => { + cy.get('[data-row=9] > .slick-cell.l5.r5').as('active_cell').click(); + cy.get('@active_cell').type('{pageup}'); + cy.get('[data-row=0] > .slick-cell.l5.r5.active').should('have.length', 1); + }); + + it('should start at Task 9 on Excess Cash column and type "PageDown" key 26x times and be on last Task 499 on same column', () => { + cy.get('[data-row=9] > .slick-cell.l5.r5').as('active_cell').click(); + let command = ''; + for (let i = 1; i <= 26; i++) { + command += '{pagedown}'; + } + cy.get('@active_cell').type(command); + cy.get('[data-row=499] > .slick-cell.l5.r5.active').should('have.length', 1); + }); + }); +}); diff --git a/docs/grid-functionalities/column-row-spanning.md b/docs/grid-functionalities/column-row-spanning.md index 451b09f42..ca5016136 100644 --- a/docs/grid-functionalities/column-row-spanning.md +++ b/docs/grid-functionalities/column-row-spanning.md @@ -1,6 +1,12 @@ ### Description You can use Colspan and/or Rowspan by using the DataView Item Metadata Provider, however please note that row spanning is under a flag because of its small perf hit (`rowspan` requires an initial loop through of all row item metadata to map all row span). +> [!NOTE] +> Please note that `colspan` and `rowspan` have multiple constraints that you must be aware, +> any side effects will **not** keep anything in sync since metadata are based on grid row index based... +> for example: Filtering/Sorting/Paging/ColumnReorder/ColumnHidding +> These side effect will require user's own logic to deal with such things! + ### Demo #### Colspan / Rowspan diff --git a/examples/vite-demo-vanilla-bundle/src/examples/example33.ts b/examples/vite-demo-vanilla-bundle/src/examples/example33.ts index 55ebf73ae..c68636356 100644 --- a/examples/vite-demo-vanilla-bundle/src/examples/example33.ts +++ b/examples/vite-demo-vanilla-bundle/src/examples/example33.ts @@ -62,14 +62,6 @@ export default class Example33 { }, }; - get slickerGridInstance() { - return this.sgb?.instances; - } - - set isFilteringEnabled(enabled: boolean) { - this.filteringEnabledClass = enabled ? 'mdi mdi-toggle-switch' : 'mdi mdi-toggle-switch-off-outline'; - } - constructor() { this._bindingEventService = new BindingEventService(); } diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/column-row-spanning.md b/frameworks/slickgrid-vue/docs/grid-functionalities/column-row-spanning.md index c2026853a..1b9cb80f3 100644 --- a/frameworks/slickgrid-vue/docs/grid-functionalities/column-row-spanning.md +++ b/frameworks/slickgrid-vue/docs/grid-functionalities/column-row-spanning.md @@ -1,6 +1,12 @@ ### Description You can use Colspan and/or Rowspan by using the DataView Item Metadata Provider, however please note that row spanning is under a flag because of its small perf hit (`rowspan` requires an initial loop through of all row item metadata to map all row span). +> [!NOTE] +> Please note that `colspan` and `rowspan` have multiple constraints that you must be aware, +> any side effects will **not** keep anything in sync since metadata are based on grid row index based... +> for example: Filtering/Sorting/Paging/ColumnReorder/ColumnHidding +> These side effect will require user's own logic to deal with such things! + ### Demo #### Colspan / Rowspan diff --git a/frameworks/slickgrid-vue/src/components/SlickgridVue.vue b/frameworks/slickgrid-vue/src/components/SlickgridVue.vue index 3c85d3a5b..04b75310f 100644 --- a/frameworks/slickgrid-vue/src/components/SlickgridVue.vue +++ b/frameworks/slickgrid-vue/src/components/SlickgridVue.vue @@ -194,7 +194,7 @@ const paginationModel = defineModel<Pagination>('pagination'); watch(paginationModel, (newPaginationOptions) => paginationOptionsChanged(newPaginationOptions!)); const _columnDefinitions = ref<Column[]>(); -const columnDefinitionsModel = defineModel<Column[]>('columns', { required: true, default: [] }); +const columnDefinitionsModel = defineModel<Column[]>('columns', { required: true, default: [] as Column[] }); watch(columnDefinitionsModel, (columnDefinitions) => columnDefinitionsChanged(columnDefinitions), { immediate: true }); const dataModel = defineModel<any[]>('data', { required: false }); // technically true but user could use datasetHierarchical instead @@ -401,7 +401,10 @@ function initialization() { } const dataviewInlineFilters = (_gridOptions.value?.dataView && _gridOptions.value.dataView.inlineFilters) || false; - let dataViewOptions: Partial<DataViewOption> = { inlineFilters: dataviewInlineFilters }; + let dataViewOptions: Partial<DataViewOption> = { + ..._gridOptions.value.dataView, + inlineFilters: dataviewInlineFilters, + } as Partial<DataViewOption>; if (_gridOptions.value?.draggableGrouping || _gridOptions.value?.enableGrouping) { groupItemMetadataProvider = new SlickGroupItemMetadataProvider(); @@ -809,7 +812,7 @@ function bindDifferentHooks(grid: SlickGrid, gridOptions: GridOption, dataView: } }); - if (gridOptions?.enableFiltering && !gridOptions.enableRowDetailView) { + if ((gridOptions?.enableFiltering || gridOptions?.dataView?.globalItemMetadataProvider) && !gridOptions.enableRowDetailView) { eventHandler.subscribe(dataView.onRowsChanged, (_e, { calledOnRowCountChanged, rows }) => { // filtering data with local dataset will not always show correctly unless we call this updateRow/render // also don't use "invalidateRows" since it destroys the entire row and as bad user experience when updating a row diff --git a/packages/common/src/interfaces/itemMetadata.interface.ts b/packages/common/src/interfaces/itemMetadata.interface.ts index b6392e6a2..1a1d2311c 100644 --- a/packages/common/src/interfaces/itemMetadata.interface.ts +++ b/packages/common/src/interfaces/itemMetadata.interface.ts @@ -10,8 +10,10 @@ export type ColumnMetadata = Pick< * and handling of a particular data item. The method should return `null` when the item requires no special handling, * or an object following the ItemMetadata interface */ +// properties describing metadata related to the item (e.g. grid row) itself export interface ItemMetadata { - // properties describing metadata related to the item (e.g. grid row) itself + /** any attribute types */ + attributes?: any; /** One or more (space-separated) CSS classes that will be added to the entire row. */ cssClasses?: string; diff --git a/test/cypress/e2e/example32.cy.ts b/test/cypress/e2e/example43.cy.ts similarity index 100% rename from test/cypress/e2e/example32.cy.ts rename to test/cypress/e2e/example43.cy.ts diff --git a/test/cypress/e2e/example33.cy.ts b/test/cypress/e2e/example44.cy.ts similarity index 100% rename from test/cypress/e2e/example33.cy.ts rename to test/cypress/e2e/example44.cy.ts From b7fddf0e365583702c073cfb8d395944d739a9c7 Mon Sep 17 00:00:00 2001 From: ghiscoding <gbeaulac@gmail.com> Date: Sat, 18 Jan 2025 17:46:43 -0500 Subject: [PATCH 2/3] chore: improve styling --- demos/vue/src/components/Example43.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/demos/vue/src/components/Example43.vue b/demos/vue/src/components/Example43.vue index 4b8d1115d..2bc77176d 100644 --- a/demos/vue/src/components/Example43.vue +++ b/demos/vue/src/components/Example43.vue @@ -508,10 +508,11 @@ function vueGridReady(grid: SlickgridVueInstance) { --slick-row-mouse-hover-box-shadow: 0; --slick-cell-odd-background-color: #fff; - // --slick-cell-border-top: 0; --slick-cell-border-right: 1px solid var(--slick-border-color); - --slick-cell-border-bottom: 1px solid var(--slick-border-color); - // --slick-cell-border-left: 1px; + --slick-cell-border-bottom: 0; + --slick-cell-border-top: 1px solid var(--slick-border-color); + --slick-header-filter-row-border-bottom: 1px solid var(--slick-border-color); + --slick-cell-border-left: 0; --slick-cell-box-shadow: none; --slick-row-mouse-hover-color: #fff; --slick-cell-display: flex; From a7e394db31b98dbefd6dc7567cf14c031ef92215 Mon Sep 17 00:00:00 2001 From: ghiscoding <gbeaulac@gmail.com> Date: Sat, 18 Jan 2025 17:48:14 -0500 Subject: [PATCH 3/3] chore: fix failing Cypress test --- demos/vue/test/cypress/e2e/example17.cy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/demos/vue/test/cypress/e2e/example17.cy.ts b/demos/vue/test/cypress/e2e/example17.cy.ts index 619d7842f..54ae19f79 100644 --- a/demos/vue/test/cypress/e2e/example17.cy.ts +++ b/demos/vue/test/cypress/e2e/example17.cy.ts @@ -1,10 +1,10 @@ -describe('Example 43 - Dynamically Create Grid from CSV / Excel import', () => { +describe('Example 17 - Dynamically Create Grid from CSV / Excel import', () => { const defaultCsvTitles = ['First Name', 'Last Name', 'Age', 'Type']; const GRID_ROW_HEIGHT = 33; it('should display Example title', () => { - cy.visit(`${Cypress.config('baseUrl')}/example43`); - cy.get('h2').should('contain', 'Example 43: Dynamically Create Grid from CSV / Excel import'); + cy.visit(`${Cypress.config('baseUrl')}/example17`); + cy.get('h2').should('contain', 'Example 17: Dynamically Create Grid from CSV / Excel import'); }); it('should load default CSV file and expect default column titles', () => {