Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(files): Ensure folders and favorites are sorted first regardless of sorting mode #41519

Merged
merged 4 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 35 additions & 16 deletions apps/files/src/views/FilesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,40 @@ export default Vue.extend({
return this.filesStore.getNode(fileId)
},

/**
* Directory content sorting parameters
* Provided by an extra computed property for caching
*/
sortingParameters() {
const identifiers = [
// 1: Sort favorites first if enabled
...(this.userConfig.sort_favorites_first ? [v => v.attributes?.favorite !== 1] : []),
// 2: Sort folders first if sorting by name
...(this.sortingMode === 'basename' ? [v => v.type !== 'folder'] : []),
// 3: Use sorting mode if NOT basename (to be able to use displayName too)
...(this.sortingMode !== 'basename' ? [v => v[this.sortingMode]] : []),
// 4: Use displayName if available, fallback to name
v => v.attributes?.displayName || v.basename,
// 5: Finally, use basename if all previous sorting methods failed
v => v.basename,
]
const orders = [
// (for 1): always sort favorites before normal files
...(this.userConfig.sort_favorites_first ? ['asc'] : []),
// (for 2): always sort folders before files
...(this.sortingMode === 'basename' ? ['asc'] : []),
// (for 3): Reverse if sorting by mtime as mtime higher means edited more recent -> lower
...(this.sortingMode === 'mtime' ? [this.isAscSorting ? 'desc' : 'asc'] : []),
// (also for 3 so make sure not to conflict with 2 and 3)
...(this.sortingMode !== 'mtime' && this.sortingMode !== 'basename' ? [this.isAscSorting ? 'asc' : 'desc'] : []),
// for 4: use configured sorting direction
this.isAscSorting ? 'asc' : 'desc',
// for 5: use configured sorting direction
this.isAscSorting ? 'asc' : 'desc',
susnux marked this conversation as resolved.
Show resolved Hide resolved
]
return [identifiers, orders] as const
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as const ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes otherwise the type would be (v => bool | string)[] using const the type is [v => bool, string] making it obvious that this only returns an array with exactly two elements. So that we can deconstruct it for orderBy

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before after
Screenshot_20231116_142714 Screenshot_20231116_143023

},

/**
* The current directory contents.
*/
Expand All @@ -234,24 +268,9 @@ export default Vue.extend({
return this.isAscSorting ? results : results.reverse()
}

const identifiers = [
// Sort favorites first if enabled
...this.userConfig.sort_favorites_first ? [v => v.attributes?.favorite !== 1] : [],
// Sort folders first if sorting by name
...this.sortingMode === 'basename' ? [v => v.type !== 'folder'] : [],
// Use sorting mode if NOT basename (to be able to use displayName too)
...this.sortingMode !== 'basename' ? [v => v[this.sortingMode]] : [],
// Use displayName if available, fallback to name
v => v.attributes?.displayName || v.basename,
// Finally, use basename if all previous sorting methods failed
v => v.basename,
]
const orders = new Array(identifiers.length).fill(this.isAscSorting ? 'asc' : 'desc')

return orderBy(
[...this.dirContents],
identifiers,
orders,
...this.sortingParameters,
)
},

Expand Down
12 changes: 3 additions & 9 deletions cypress/e2e/files.cy.ts → cypress/e2e/files/files.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
describe('Login with a new user and open the files app', function() {
before(function() {
describe('Files', { testIsolation: true }, () => {
beforeEach(() => {
cy.createRandomUser().then((user) => {
cy.login(user)
})
})

after(function() {
cy.logout()
})

it('See the default file welcome.txt in the files list', function() {
it('Login with a user and open the files app', () => {
cy.visit('/apps/files')
cy.get('[data-cy-files-list] [data-cy-files-list-row-name="welcome.txt"]').should('be.visible')
// eslint-disable-next-line cypress/no-unnecessary-waiting -- Wait for all to finish loading
cy.wait(500)
})
})
262 changes: 262 additions & 0 deletions cypress/e2e/files/files_sorting.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
/**
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
describe('Files: Sorting the file list', { testIsolation: true }, () => {
let currentUser
beforeEach(() => {
cy.createRandomUser().then((user) => {
currentUser = user
cy.login(user)
})
})

it('Files are sorted by name ascending by default', () => {
cy.uploadContent(currentUser, new Blob(), 'text/plain', '/1 first.txt')
.uploadContent(currentUser, new Blob(), 'text/plain', '/z last.txt')
.uploadContent(currentUser, new Blob(), 'text/plain', '/A.txt')
.uploadContent(currentUser, new Blob(), 'text/plain', '/Ä.txt')
.mkdir(currentUser, '/m')
.mkdir(currentUser, '/4')
cy.login(currentUser)
cy.visit('/apps/files')

cy.get('[data-cy-files-list-row]').each(($row, index) => {
switch (index) {
case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('4')
break
case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('m')
break
case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('1 first.txt')
break
case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('A.txt')
break
case 4: expect($row.attr('data-cy-files-list-row-name')).to.eq('Ä.txt')
break
case 5: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt')
break
case 6: expect($row.attr('data-cy-files-list-row-name')).to.eq('z last.txt')
break
}
})
})

it('Can sort by size', () => {
cy.uploadContent(currentUser, new Blob(), 'text/plain', '/1 tiny.txt')
.uploadContent(currentUser, new Blob(['a'.repeat(1024)]), 'text/plain', '/z big.txt')
.uploadContent(currentUser, new Blob(['a'.repeat(512)]), 'text/plain', '/a medium.txt')
.mkdir(currentUser, '/folder')
cy.login(currentUser)
cy.visit('/apps/files')

// click sort button
cy.get('th').contains('button', 'Size').click()
// sorting is set
cy.contains('th', 'Size').should('have.attr', 'aria-sort', 'ascending')
// Files are sorted
cy.get('[data-cy-files-list-row]').each(($row, index) => {
switch (index) {
case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('1 tiny.txt')
break
case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('folder')
break
case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt')
break
case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('a medium.txt')
break
case 4: expect($row.attr('data-cy-files-list-row-name')).to.eq('z big.txt')
break
}
})

// click sort button
cy.get('th').contains('button', 'Size').click()
// sorting is set
cy.contains('th', 'Size').should('have.attr', 'aria-sort', 'descending')
// Files are sorted
cy.get('[data-cy-files-list-row]').each(($row, index) => {
switch (index) {
case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('z big.txt')
break
case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('a medium.txt')
break
case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt')
break
case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('folder')
break
case 4: expect($row.attr('data-cy-files-list-row-name')).to.eq('1 tiny.txt')
break
}
})
})

it('Can sort by mtime', () => {
cy.uploadContent(currentUser, new Blob(), 'text/plain', '/1.txt', Date.now() / 1000 - 86400 - 1000)
.uploadContent(currentUser, new Blob(['a'.repeat(1024)]), 'text/plain', '/z.txt', Date.now() / 1000 - 86400)
.uploadContent(currentUser, new Blob(['a'.repeat(512)]), 'text/plain', '/a.txt', Date.now() / 1000 - 86400 - 500)
cy.login(currentUser)
cy.visit('/apps/files')

// click sort button
cy.get('th').contains('button', 'Modified').click()
// sorting is set
cy.contains('th', 'Modified').should('have.attr', 'aria-sort', 'ascending')
// Files are sorted
cy.get('[data-cy-files-list-row]').each(($row, index) => {
switch (index) {
case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') // uploaded right now
break
case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt') // fake time of yesterday
break
case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt') // fake time of yesterday and few minutes
break
case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt') // fake time of yesterday and ~15 minutes ago
break
}
})

// reverse order
cy.get('th').contains('button', 'Modified').click()
cy.contains('th', 'Modified').should('have.attr', 'aria-sort', 'descending')
cy.get('[data-cy-files-list-row]').each(($row, index) => {
switch (index) {
case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt') // uploaded right now
break
case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt') // fake time of yesterday
break
case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt') // fake time of yesterday and few minutes
break
case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt') // fake time of yesterday and ~15 minutes ago
break
}
})
})

it('Favorites are sorted first', () => {
cy.uploadContent(currentUser, new Blob(), 'text/plain', '/1.txt', Date.now() / 1000 - 86400 - 1000)
.uploadContent(currentUser, new Blob(['a'.repeat(1024)]), 'text/plain', '/z.txt', Date.now() / 1000 - 86400)
.uploadContent(currentUser, new Blob(['a'.repeat(512)]), 'text/plain', '/a.txt', Date.now() / 1000 - 86400 - 500)
.setFileAsFavorite(currentUser, '/a.txt')
cy.login(currentUser)
cy.visit('/apps/files')

cy.log('By name - ascending')
cy.get('th').contains('button', 'Name').click()
cy.contains('th', 'Name').should('have.attr', 'aria-sort', 'ascending')

cy.get('[data-cy-files-list-row]').each(($row, index) => {
switch (index) {
case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt')
break
case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt')
break
case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt')
break
case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt')
break
}
})

cy.log('By name - descending')
cy.get('th').contains('button', 'Name').click()
cy.contains('th', 'Name').should('have.attr', 'aria-sort', 'descending')

cy.get('[data-cy-files-list-row]').each(($row, index) => {
switch (index) {
case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt')
break
case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt')
break
case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt')
break
case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt')
break
}
})

cy.log('By size - ascending')
cy.get('th').contains('button', 'Size').click()
cy.contains('th', 'Size').should('have.attr', 'aria-sort', 'ascending')

cy.get('[data-cy-files-list-row]').each(($row, index) => {
switch (index) {
case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt')
break
case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt')
break
case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt')
break
case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt')
break
}
})

cy.log('By size - descending')
cy.get('th').contains('button', 'Size').click()
cy.contains('th', 'Size').should('have.attr', 'aria-sort', 'descending')

cy.get('[data-cy-files-list-row]').each(($row, index) => {
switch (index) {
case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt')
break
case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt')
break
case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt')
break
case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt')
break
}
})

cy.log('By mtime - ascending')
cy.get('th').contains('button', 'Modified').click()
cy.contains('th', 'Modified').should('have.attr', 'aria-sort', 'ascending')

cy.get('[data-cy-files-list-row]').each(($row, index) => {
switch (index) {
case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt')
break
case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt')
break
case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt')
break
case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt')
break
}
})

cy.log('By mtime - descending')
cy.get('th').contains('button', 'Modified').click()
cy.contains('th', 'Modified').should('have.attr', 'aria-sort', 'descending')

cy.get('[data-cy-files-list-row]').each(($row, index) => {
switch (index) {
case 0: expect($row.attr('data-cy-files-list-row-name')).to.eq('a.txt')
break
case 1: expect($row.attr('data-cy-files-list-row-name')).to.eq('1.txt')
break
case 2: expect($row.attr('data-cy-files-list-row-name')).to.eq('z.txt')
break
case 3: expect($row.attr('data-cy-files-list-row-name')).to.eq('welcome.txt')
break
}
})
})
})
Loading
Loading