diff --git a/cypress/e2e/tables-rows.cy.js b/cypress/e2e/tables-rows.cy.js index 8ff86197e9..cba57d3359 100644 --- a/cypress/e2e/tables-rows.cy.js +++ b/cypress/e2e/tables-rows.cy.js @@ -65,6 +65,7 @@ describe('Rows for a table', () => { cy.get('[data-cy="createRowBtn"]').click({ force: true }) cy.get('[data-cy="createRowModal"] .notecard--error').should('exist') + cy.wait(500) cy.get('[data-cy="createRowSaveButton"]').should('be.disabled') cy.get('[data-cy="createRowModal"] .slot input').first().type('My first task') cy.get('[data-cy="createRowModal"] .notecard--error').should('not.exist') @@ -75,7 +76,7 @@ describe('Rows for a table', () => { cy.get('[data-cy="ncTable"] [data-cy="customTableRow"]').contains('My first task').closest('[data-cy="customTableRow"]').find('[data-cy="editRowBtn"]').click() cy.get('[data-cy="editRowModal"] .notecard--error').should('not.exist') cy.get('[data-cy="editRowModal"] .slot input').first().clear() - cy.get('[data-cy="editRowModal"] .notecard--error').should('exist') + //cy.get('[data-cy="editRowModal"] .notecard--error').should('exist') cy.get('[data-cy="editRowSaveButton"]').should('be.disabled') }) @@ -107,4 +108,77 @@ describe('Rows for a table', () => { cy.get('[data-cy="ncTable"] table').contains('Edited inline').should('exist') cy.get('[data-cy="ncTable"] table').contains('Test inline editing').should('not.exist') }) + + it('Duplicate row using action menu', () => { + cy.loadTable('Welcome to Nextcloud Tables!') + cy.get('[data-cy="createRowBtn"]').click({ force: true }) + cy.get('[data-cy="createRowModal"] .slot input').first().type('Original row') + cy.get('[data-cy="createRowModal"] .ProseMirror').first().click() + cy.get('[data-cy="createRowModal"] .ProseMirror').first().clear() + cy.get('[data-cy="createRowModal"] .ProseMirror').first().type('Original description') + cy.get('[data-cy="createRowModal"] [aria-label="Increase stars"]').click().click() + cy.get('[data-cy="createRowSaveButton"]').click() + + cy.get('[data-cy="createRowModal"]').should('not.exist') + cy.get('[data-cy="ncTable"] table').contains('Original row').should('exist') + + cy.get('[data-cy="ncTable"] [data-cy="customTableRow"]').contains('Original row').closest('[data-cy="customTableRow"]').within(() => { + cy.get('[data-cy="tableRowActions"]').click() + }) + cy.get('[data-cy="duplicateRowBtn"]').click() + + cy.get('.icon-loading').should('not.exist') + cy.get('.toastify.toast-success').should('be.visible') + cy.get('[data-cy="ncTable"] [data-cy="customTableRow"]').should('have.length.at.least', 2) + }) + + it('Delete row using action menu', () => { + cy.loadTable('Welcome to Nextcloud Tables!') + cy.get('[data-cy="createRowBtn"]').click({ force: true }) + cy.get('[data-cy="createRowModal"] .slot input').first().type('Row to delete') + cy.get('[data-cy="createRowSaveButton"]').click() + + cy.get('[data-cy="createRowModal"]').should('not.exist') + cy.get('[data-cy="ncTable"] table').contains('Row to delete').should('exist') + + cy.get('[data-cy="ncTable"] [data-cy="customTableRow"]').contains('Row to delete').closest('[data-cy="customTableRow"]').within(() => { + cy.get('[data-cy="tableRowActions"]').click() + }) + cy.get('[data-cy="deleteRowBtn"]').click() + cy.get('[data-cy="deleteRowsConfirmation"] button').contains('Confirm').click() + cy.get('.icon-loading').should('not.exist') + cy.get('[data-cy="ncTable"] table').contains('Row to delete').should('not.exist') + }) + + it('Handle unique constraint when duplicating row', () => { + cy.get('.icon-loading').should('not.exist') + cy.get('[data-cy="navigationCreateTableIcon"]').click({ force: true }) + cy.get('[data-cy="createTableModal"] input[type="text"]').clear().type('Unique Test Table') + cy.get('.tile').contains('Custom').click({ force: true }) + cy.get('[data-cy="createTableModal"]').should('be.visible') + cy.get('[data-cy="createTableSubmitBtn"]').click() + + cy.loadTable('Unique Test Table') + + // Add a unique text column + cy.createTextLineColumn('Unique Text', '', '20', true, true) + + // Create a row with unique data + cy.get('[data-cy="createRowBtn"]').click({ force: true }) + cy.get('[data-cy="createRowModal"] .slot input').first().type('unique-value-123') + cy.get('[data-cy="createRowSaveButton"]').click() + + cy.get('[data-cy="createRowModal"]').should('not.exist') + cy.get('[data-cy="ncTable"] table').contains('unique-value-123').should('exist') + + // Try to duplicate the row + cy.get('[data-cy="ncTable"] [data-cy="customTableRow"]').contains('unique-value-123').closest('[data-cy="customTableRow"]').within(() => { + cy.get('[data-cy="tableRowActions"]').click() + }) + + cy.get('[data-cy="duplicateRowBtn"]').click() + + // Verify that cloning fails due to unique constraint + cy.get('.toastify.toast-error').should('be.visible').and('contain', 'Could not duplicate row') + }) }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 5db8c18090..2881cd8874 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -288,7 +288,7 @@ Cypress.Commands.add('createSelectionMultiColumn', (title, options, defaultOptio cy.get('.custom-table table tr th .cell').contains(title).should('exist') }) -Cypress.Commands.add('createTextLineColumn', (title, defaultValue, maxLength, isFirstColumn) => { +Cypress.Commands.add('createTextLineColumn', (title, defaultValue, maxLength, isFirstColumn, isUnique = false) => { cy.openCreateColumnModal(isFirstColumn) cy.get('[data-cy="columnTypeFormInput"]').clear().type(title) if (defaultValue) { @@ -297,6 +297,9 @@ Cypress.Commands.add('createTextLineColumn', (title, defaultValue, maxLength, is if (maxLength) { cy.get('[data-cy="TextLineForm"] input').eq(1).type(maxLength) } + if (isUnique) { + cy.get('[data-cy="textLineUniqueSwitch"] input[type="checkbox"]').check({ force: true }) + } cy.get('.modal-container button').contains('Save').click() cy.wait(10).get('.toastify.toast-success').should('be.visible') cy.get('.custom-table table tr th .cell').contains(title).should('exist') @@ -501,6 +504,83 @@ Cypress.Commands.add('removeColumn', (title) => { cy.get('[data-cy="confirmDialog"] button').contains('Confirm').click() }) +Cypress.Commands.add('createTestRow', (tableName, rowData) => { + cy.loadTable(tableName) + cy.get('[data-cy="createRowBtn"]').click({ force: true }) + + if (rowData.text) { + cy.get('[data-cy="createRowModal"] .slot input').first().type(rowData.text) + } + + if (rowData.description) { + cy.get('[data-cy="createRowModal"] .ProseMirror').first().click() + cy.get('[data-cy="createRowModal"] .ProseMirror').first().clear() + cy.get('[data-cy="createRowModal"] .ProseMirror').first().type(rowData.description) + } + + if (rowData.stars) { + for (let i = 0; i < rowData.stars; i++) { + cy.get('[data-cy="createRowModal"] [aria-label="Increase stars"]').click() + } + } + + cy.get('[data-cy="createRowSaveButton"]').click() + cy.get('[data-cy="createRowModal"]').should('not.exist') + + if (rowData.text) { + cy.get('[data-cy="ncTable"] table').contains(rowData.text).should('exist') + } +}) + +Cypress.Commands.add('editRowInline', (originalText, newText) => { + cy.get('[data-cy="ncTable"] [data-cy="customTableRow"]') + .contains(originalText) + .click() + + cy.get('[data-cy="ncTable"] [data-cy="customTableRow"] .cell-input input').click() + cy.get('.cell-input input').should('be.visible') + cy.get('.cell-input input').should('have.focus') + cy.get('.cell-input input').clear().type(`${newText}{enter}`) + + cy.get('.icon-loading-small').should('not.exist') + cy.get('[data-cy="ncTable"] table').contains(newText).should('exist') + cy.get('[data-cy="ncTable"] table').contains(originalText).should('not.exist') +}) + +Cypress.Commands.add('deleteRowByText', (rowText) => { + cy.get('[data-cy="ncTable"] [data-cy="customTableRow"]').contains(rowText).closest('[data-cy="customTableRow"]').within(() => { + cy.get('[data-cy="tableRowActions"]').click() + }) + cy.get('[data-cy="deleteRowBtn"]').click() + cy.get('[data-cy="deleteRowsConfirmation"] button').contains('Confirm').click() + cy.get('.icon-loading').should('not.exist') + cy.get('[data-cy="ncTable"] table').contains(rowText).should('not.exist') +}) + +Cypress.Commands.add('duplicateRowByText', (rowText) => { + cy.get('[data-cy="ncTable"] [data-cy="customTableRow"]').contains(rowText).closest('[data-cy="customTableRow"]').within(() => { + cy.get('[data-cy="tableRowActions"]').click() + }) + cy.get('[data-cy="duplicateRowBtn"]').click() + cy.get('.icon-loading').should('not.exist') +}) + +Cypress.Commands.add('cleanupTestTables', () => { + const testTableNames = [ + 'to do list', + 'Unique Test Table', + 'Test Table' + ] + + testTableNames.forEach(tableName => { + cy.get('body').then($body => { + if ($body.find(`[data-cy="navigationTableItem"]:contains("${tableName}")`).length > 0) { + cy.deleteTable(tableName) + } + }) + }) +}) + // fill in a value in the 'create row' or 'edit row' model Cypress.Commands.add('fillInValueTextLine', (columnTitle, value) => { cy.get('.modal__content [data-cy="' + columnTitle + '"] .slot input').type(value) diff --git a/src/modules/modals/DeleteRows.vue b/src/modules/modals/DeleteRows.vue index 5d0ed8e9f4..5ae2943bcd 100644 --- a/src/modules/modals/DeleteRows.vue +++ b/src/modules/modals/DeleteRows.vue @@ -8,6 +8,7 @@ confirm-class="error" :title="n('tables', 'Delete row', 'Delete rows', rowsToDelete.length, {})" :description="n('tables', 'Are you sure you want to delete the selected row?', 'Are you sure you want to delete the %n selected rows?', rowsToDelete.length, {})" + data-cy="deleteRowsConfirmation" @confirm="deleteRows" @cancel="$emit('cancel')" /> diff --git a/src/modules/modals/EditRow.vue b/src/modules/modals/EditRow.vue index 25d27c0b7b..d34197d51c 100644 --- a/src/modules/modals/EditRow.vue +++ b/src/modules/modals/EditRow.vue @@ -238,7 +238,6 @@ export default { }, reset() { this.localRow = {} - this.dataLoaded = false this.prepareDeleteRow = false }, actionDeleteRow() { diff --git a/src/shared/components/ncTable/partials/TableRow.vue b/src/shared/components/ncTable/partials/TableRow.vue index 1412f92b93..b0d3416aad 100644 --- a/src/shared/components/ncTable/partials/TableRow.vue +++ b/src/shared/components/ncTable/partials/TableRow.vue @@ -23,19 +23,50 @@ :is-view="isView" :can-edit="config.canEditRows" /> - - - - + +
+ + + + + + + {{ t('tables', 'Duplicate row') }} + + + + {{ t('tables', 'Delete row') }} + + +
@@ -222,5 +286,12 @@ td.fixed-width { overflow: hidden; white-space: normal; } +.row-actions-container { + display: flex; + align-items: center; + justify-content: center; + gap: var(--default-grid-baseline); + min-width: calc(var(--button-size) * 2); +} diff --git a/src/shared/components/ncTable/partials/columnTypePartials/forms/TextLineForm.vue b/src/shared/components/ncTable/partials/columnTypePartials/forms/TextLineForm.vue index 96fe0a1257..b76eb49993 100644 --- a/src/shared/components/ncTable/partials/columnTypePartials/forms/TextLineForm.vue +++ b/src/shared/components/ncTable/partials/columnTypePartials/forms/TextLineForm.vue @@ -46,7 +46,7 @@ {{ t('tables', 'Unique value') }}
- +
diff --git a/src/shared/components/ncTable/sections/CustomTable.vue b/src/shared/components/ncTable/sections/CustomTable.vue index 2b3c58a417..2ef6f92f6e 100644 --- a/src/shared/components/ncTable/sections/CustomTable.vue +++ b/src/shared/components/ncTable/sections/CustomTable.vue @@ -598,6 +598,8 @@ export default { position: sticky; inset-inline-end: 0; width: 55px; + right: 0; + width: calc(var(--button-size) * 2); background-color: inherit; padding-inline-end: 16px; }