Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8b740a9
add mandatory key to ViewColumnInformation
silverkszlo Sep 11, 2025
97dd915
add mandatory key to viewsettings
silverkszlo Sep 11, 2025
60e2c80
check for mandatory in view col in rowhelper
silverkszlo Sep 11, 2025
402e6ba
check for mandatory view cols in different types
silverkszlo Sep 11, 2025
c10b4fb
check for mandatory view cols in row creation and editing
silverkszlo Sep 11, 2025
19a9774
fix: implement mandatory column validation in row data processing
enjeck Sep 14, 2025
05e3afc
fix: redundant check
enjeck Sep 14, 2025
aa74524
refactor mandatory columns validation
silverkszlo Sep 15, 2025
f78a5c9
correct typo in viewsettings
silverkszlo Sep 16, 2025
b9fb79e
fix: mandatory datetime fields can not be cleared and saved anymore
silverkszlo Sep 16, 2025
6ec6dc1
refactor rowHelper
silverkszlo Sep 17, 2025
805577d
add column watchers and fix viewColumnInformation reactivity in views…
silverkszlo Sep 17, 2025
acabde9
add cypress tests for mandatory state in view columns
silverkszlo Sep 17, 2025
d02e0f5
add mandatory to ResponseDefinitions and generate openapi spec
silverkszlo Sep 17, 2025
d8240af
run php cs linter
silverkszlo Sep 17, 2025
d5fe8d2
remove side effects of mandatory and readonly states
silverkszlo Sep 18, 2025
4176bb2
adjust cypress tests after previous typo correction in viewsettings
silverkszlo Sep 18, 2025
3dcac43
fix unrelated failing view cypress test by restructuring
silverkszlo Sep 18, 2025
35722be
add individual view titles in view cypress test
silverkszlo Sep 22, 2025
89e4edb
reduce amount of tests in mandatory state testing
silverkszlo Sep 22, 2025
aa67d6c
validate mandatory usergroup types
silverkszlo Sep 24, 2025
2f19064
fix: Properly validate selection-check type
enjeck Sep 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cypress/e2e/view-filtering-selection.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ describe('Filtering in a view by selection columns', () => {

// ## update view
cy.intercept({ method: 'PUT', url: '**/apps/tables/view/*' }).as('updateView')
cy.contains('button', 'Save View').click()
cy.contains('button', 'Save modified View').click()
cy.wait('@updateView')

// # check for expected rows
Expand Down
149 changes: 149 additions & 0 deletions cypress/e2e/view-mandatory-state.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
let localUser

describe('Mandatory Column Functionality', () => {

before(function() {
cy.createRandomUser().then(user => {
localUser = user
})
})

beforeEach(function() {
cy.login(localUser)
cy.visit('apps/tables')
})

it('Setup table with mandatory test columns and one row', () => {
cy.createTable('Mandatory test table')
cy.createTextLineColumn('title', null, null, true)
cy.createTextLineColumn('description', null, null, false)

// create one row
cy.get('[data-cy="createRowBtn"]').click()
cy.fillInValueTextLine('title', 'first row')
cy.fillInValueTextLine('description', 'desc 1')
cy.get('[data-cy="createRowSaveButton"]').click()

// create a default view
cy.get('[data-cy="customTableAction"] button').click()
cy.get('.v-popper__popper li button span')
.contains('Create view')
.click({ force: true })
cy.get('[data-cy="viewSettingsDialog"]').should('be.visible')
cy.get('[data-cy="viewSettingsDialogSection"] input').type('Mandatory test view')
cy.get('[data-cy="modifyViewBtn"]').click()
cy.get('.icon-loading').should('not.exist')
})

describe('SelectedViewColumns - Mandatory Checkbox', () => {
beforeEach(() => {
cy.loadTable('Mandatory test table')

// create a new view
cy.get('[data-cy="customTableAction"] button').click()
cy.get('[data-cy="dataTableCreateViewBtn"]').contains('Create view').click({ force: true })
cy.get('[data-cy="viewSettingsDialogSection"] input').type('Mandatory test view')

// ensure dialog is visible
cy.get('[data-cy="viewSettingsDialog"]').should('be.visible')
})

const openColumnMenu = (columnTitle) => {
cy.contains('.column-entry', columnTitle)
.find('[data-cy="customColumnAction"] button')
.click({ force: true })
}

const getMandatoryCheckbox = () => cy.get('[data-cy="columnMandatoryCheckbox"]').contains('Mandatory')

const getReadonlyCheckbox = () => cy.get('[data-cy="columnReadonlyCheckbox"]').contains('Read only')

it('should display mandatory checkbox for selected columns', () => {
openColumnMenu('title')
getMandatoryCheckbox().should('be.visible')
})

it('should disable mandatory checkbox when readonly is enabled', () => {
openColumnMenu('title')

getReadonlyCheckbox().should('be.visible').click({ force: true })

// Check that the readonly checkbox is checked
cy.get('[data-cy="columnReadonlyCheckbox"] input').should('be.checked')

// Check that mandatory checkbox input is disabled
cy.get('[data-cy="columnMandatoryCheckbox"] input').should('be.disabled')
})

it('should disable readonly checkbox when mandatory is enabled', () => {
openColumnMenu('title')

getMandatoryCheckbox().should('be.visible').click({ force: true })

// Check that the mandatory checkbox is checked
cy.get('[data-cy="columnMandatoryCheckbox"] input').should('be.checked')

// Check that readonly checkbox input is disabled
cy.get('[data-cy="columnReadonlyCheckbox"] input').should('be.disabled')
})
})

describe('EditRow - Mandatory Field Validation', () => {
beforeEach(() => {
cy.loadTable('Mandatory test table')

// Create a view with mandatory settings first
cy.get('[data-cy="customTableAction"] button').click()
cy.get('[data-cy="dataTableCreateViewBtn"]').contains('Create view').click({ force: true })
cy.get('[data-cy="viewSettingsDialogSection"] input').type('Mandatory validation test view')

// Set title column as mandatory in the view
cy.contains('.column-entry', 'title')
.find('[data-cy="customColumnAction"] button')
.click({ force: true })
cy.get('[data-cy="columnMandatoryCheckbox"]').contains('Mandatory').click({ force: true })

// Save the view
cy.get('[data-cy="modifyViewBtn"]').click()
cy.get('.icon-loading').should('not.exist')

// Now open edit row dialog
cy.get('[data-cy="editRowBtn"]').first().click()
cy.get('[data-cy="editRowModal"]').should('be.visible')
})

it('should show error when mandatory field is empty', () => {
cy.get('[data-cy="editRowModal"] input').first().clear().blur()

// Try multiple possible selectors for NcNoteCard with type="error"
cy.get('[data-cy="editRowModal"]').within(() => {
// Try different possible selectors
cy.get('.notecard--error, .note-card--error, [type="error"], .notecard[type="error"], .error', { timeout: 5000 })
.should('exist')
})
})

it('should disable save button when mandatory field is empty', () => {
// Clear the mandatory field (should be the title field which is mandatory)
cy.get('[data-cy="editRowModal"] input').first().clear()

// Trigger validation by blurring and maybe clicking somewhere else
cy.get('[data-cy="editRowModal"] input').first().blur()

// Wait a bit for validation to process
cy.wait(500)

// Check that save button is disabled
cy.get('[data-cy="editRowSaveButton"]', { timeout: 5000 }).should('be.disabled')
})

it('should enable save button when mandatory field is filled', () => {
cy.get('[data-cy="editRowModal"] input').first().type('filled value')
cy.get('[data-cy="editRowSaveButton"]', { timeout: 5000 }).should('not.be.disabled')
})
})
})
64 changes: 47 additions & 17 deletions cypress/e2e/view.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
let localUser
const title = 'Test view'
const firstTitle = 'Test view'
const secondTitle = 'Test view 2'
const thirdTitle = 'Test view 3'
const fourthTitle = 'Test view 4'

describe('Interact with views', () => {

Expand All @@ -16,7 +19,9 @@ describe('Interact with views', () => {
beforeEach(function() {
cy.login(localUser)
cy.visit('apps/tables')
})

it('Setup table', () => {
cy.createTable('View test table')
cy.createTextLineColumn('title', null, null, true)
cy.createSelectionColumn('selection', ['sel1', 'sel2', 'sel3', 'sel4'], null, false)
Expand All @@ -38,15 +43,10 @@ describe('Interact with views', () => {
cy.fillInValueTextLine('title', 'sevenths row')
cy.fillInValueSelection('selection', 'sel2')
cy.get('[data-cy="createRowSaveButton"]').click()

// create view
cy.get('[data-cy="customTableAction"] button').click()
cy.get('[data-cy="dataTableCreateViewBtn"]').contains('Create view').click({ force: true })
cy.get('[data-cy="viewSettingsDialogSection"] input').type(title)
})

// cleanup
afterEach(function() {
// cleanup after all tests
after(function() {
// delete table (with view)
cy.get('[data-cy="navigationTableItem"]').contains('View test table').click({ force: true })
cy.get('[data-cy="customTableAction"] button').click()
Comment thread
silverkszlo marked this conversation as resolved.
Expand All @@ -55,11 +55,20 @@ describe('Interact with views', () => {
cy.get('[data-cy="editTableModal"] [data-cy="editTableDeleteBtn"]').click()
cy.get('[data-cy="editTableModal"] [data-cy="editTableConfirmDeleteBtn"]').click()
cy.wait(10).get('.toastify.toast-success').should('be.visible')
cy.get('[data-cy="navigationTableItem"]').contains('View test table').should('not.exist')
cy.get('[data-cy="navigationTableItem"]').contains(title).should('not.exist')
cy.get('[data-cy="navigationTableItem"]').contains(firstTitle).should('not.exist')
cy.get('[data-cy="navigationTableItem"]').contains(secondTitle).should('not.exist')
cy.get('[data-cy="navigationTableItem"]').contains(thirdTitle).should('not.exist')
cy.get('[data-cy="navigationTableItem"]').contains(fourthTitle).should('not.exist')
})

it('Create view and insert rows in the view', () => {
cy.loadTable('View test table')

// create view
cy.get('[data-cy="customTableAction"] button').click()
cy.get('[data-cy="dataTableCreateViewBtn"]').contains('Create view').click({ force: true })
cy.get('[data-cy="viewSettingsDialogSection"] input').type(firstTitle)

// ## add filter
cy.get('[data-cy="filterFormFilterGroupBtn"]').contains('Add new filter group').click()
cy.get('[data-cy="filterEntryColumn"]').click()
Expand All @@ -75,7 +84,7 @@ describe('Interact with views', () => {
cy.get('[data-cy="modifyViewBtn"]').contains('Create View').click()
cy.wait('@createView')
cy.wait('@updateView')
cy.get('[data-cy="navigationViewItem"]').contains(title).should('exist')
cy.get('[data-cy="navigationViewItem"]').contains(firstTitle).should('exist')

const expected = ['sevenths row', 'second row']
expected.forEach(item => {
Expand All @@ -95,13 +104,20 @@ describe('Interact with views', () => {
})

it('Create view and update rows in the view', () => {
cy.loadTable('View test table')

// create view
cy.get('[data-cy="customTableAction"] button').click()
cy.get('[data-cy="dataTableCreateViewBtn"]').contains('Create view').click({ force: true })
cy.get('[data-cy="viewSettingsDialogSection"] input').type(secondTitle)

// ## save view
cy.intercept({ method: 'POST', url: '**/apps/tables/view' }).as('createView')
cy.intercept({ method: 'PUT', url: '**/apps/tables/view/*' }).as('updateView')
cy.get('[data-cy="modifyViewBtn"]').contains('Create View').click()
cy.wait('@createView')
cy.wait('@updateView')
cy.get('[data-cy="navigationViewItem"]').contains(title).should('exist')
cy.get('[data-cy="navigationViewItem"]').contains(secondTitle).should('exist')

// Update rows in the view
cy.get('[data-cy="customTableRow"]').contains('first row').closest('[data-cy="customTableRow"]').find('[data-cy="editRowBtn"]').click()
Expand All @@ -114,6 +130,13 @@ describe('Interact with views', () => {
})

it('Create view and make column readonly in the view', () => {
cy.loadTable('View test table')

// create view
cy.get('[data-cy="customTableAction"] button').click()
cy.get('[data-cy="dataTableCreateViewBtn"]').contains('Create view').click({ force: true })
cy.get('[data-cy="viewSettingsDialogSection"] input').type(thirdTitle)

// trigger three dot menu and select readonly
cy.contains('.column-entry', 'title').find('[data-cy="customColumnAction"] button').click({ force: true })
cy.get('[data-cy="columnReadonlyCheckbox"]').contains('Read only').click()
Expand All @@ -125,7 +148,7 @@ describe('Interact with views', () => {
cy.wait('@createView')
cy.wait('@updateView')

cy.get('[data-cy="navigationViewItem"]').contains(title).should('exist')
cy.get('[data-cy="navigationViewItem"]').contains(thirdTitle).should('exist')

// TODO: Make sure that column is readonly during edit
// cy.get('[data-cy="customTableRow"]').contains('first row').closest('[data-cy="customTableRow"]').find('[data-cy="editRowBtn"]').click()
Expand All @@ -134,22 +157,29 @@ describe('Interact with views', () => {
})

it('Create view and delete rows in the view', () => {
cy.loadTable('View test table')

// create view
cy.get('[data-cy="customTableAction"] button').click()
cy.get('[data-cy="dataTableCreateViewBtn"]').contains('Create view').click({ force: true })
cy.get('[data-cy="viewSettingsDialogSection"] input').type(fourthTitle)

// ## save view
cy.intercept({ method: 'POST', url: '**/apps/tables/view' }).as('createView')
cy.intercept({ method: 'PUT', url: '**/apps/tables/view/*' }).as('updateView')
cy.get('[data-cy="modifyViewBtn"]').contains('Create View').click()
cy.wait('@createView')
cy.wait('@updateView')
cy.get('[data-cy="navigationViewItem"]').contains(title).should('exist')
cy.get('[data-cy="navigationViewItem"]').contains(fourthTitle).should('exist')
cy.get('.icon-loading').should('not.exist')

// Delete rows in the view
cy.get('[data-cy="customTableRow"]').contains('first row').closest('[data-cy="customTableRow"]').find('[data-cy="editRowBtn"]').click()
// Delete the first row (whatever it is)
cy.get('[data-cy="customTableRow"]').first().find('[data-cy="editRowBtn"]').click()
cy.get('[data-cy="editRowModal"] [data-cy="editRowDeleteButton"]').click()
cy.get('[data-cy="editRowModal"] [data-cy="editRowDeleteConfirmButton"]').click()

cy.get('[data-cy="editRowModal"]').should('not.exist')
cy.get('[data-cy="customTableRow"]').contains('first row').should('not.exist')
// Verify one row was deleted by checking the count decreased
cy.get('[data-cy="customTableRow"]').should('have.length.lessThan', 4)
})
})
1 change: 1 addition & 0 deletions lib/Db/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class Column extends EntitySuper implements JsonSerializable {
public const SUBTYPE_DATETIME_TIME = 'time';

public const SUBTYPE_SELECTION_CHECK = 'check';
public const SUBTYPE_SELECTION_MULTI = 'selection-multi';

public const SUBTYPE_TEXT_LINE = 'line';

Expand Down
1 change: 1 addition & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
* columnId: int,
* order: int,
* readonly: bool,
* mandatory: bool,
* },
* customSettings: ?array{
* width: int,
Expand Down
Loading
Loading