diff --git a/composer.json b/composer.json index d74a04a00..c151ae396 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "nextcloud/coding-standard": "^1.0", "nextcloud/ocp": "dev-stable28", "phan/phan": "^5", - "php-cs-fixer/shim": "3.95.1", + "php-cs-fixer/shim": "3.94.2", "psalm/phar": "^5.26", "squizlabs/php_codesniffer": "^4", "staabm/annotate-pull-request-from-checkstyle": "^1.1.0" diff --git a/composer.lock b/composer.lock index 91efa7cea..16ad85cf8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "59ee4d7a0f7a5c289e698bf23f1f4ecb", + "content-hash": "2c350e99ff300df7f89b8b2c6e919726", "packages": [], "packages-dev": [ { @@ -966,16 +966,16 @@ }, { "name": "php-cs-fixer/shim", - "version": "v3.95.1", + "version": "v3.94.2", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/shim.git", - "reference": "f81ccf51ca60cc9dd21358ffba0e79ebd2ebb78a" + "reference": "80fd29f44a736136a2f05bae5464816a444b91d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/shim/zipball/f81ccf51ca60cc9dd21358ffba0e79ebd2ebb78a", - "reference": "f81ccf51ca60cc9dd21358ffba0e79ebd2ebb78a", + "url": "https://api.github.com/repos/PHP-CS-Fixer/shim/zipball/80fd29f44a736136a2f05bae5464816a444b91d1", + "reference": "80fd29f44a736136a2f05bae5464816a444b91d1", "shasum": "" }, "require": { @@ -1012,9 +1012,9 @@ "description": "A tool to automatically fix PHP code style", "support": { "issues": "https://github.com/PHP-CS-Fixer/shim/issues", - "source": "https://github.com/PHP-CS-Fixer/shim/tree/v3.95.1" + "source": "https://github.com/PHP-CS-Fixer/shim/tree/v3.94.2" }, - "time": "2026-04-12T17:00:34+00:00" + "time": "2026-02-20T16:14:17+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -2097,16 +2097,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.37.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { @@ -2158,7 +2158,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -2178,20 +2178,20 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.37.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", - "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -2242,7 +2242,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -2262,7 +2262,7 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "tysonandre/var_representation_polyfill", diff --git a/playwright/e2e/category-actions.spec.ts b/playwright/e2e/category-actions.spec.ts index 76d61c320..302503e34 100644 --- a/playwright/e2e/category-actions.spec.ts +++ b/playwright/e2e/category-actions.spec.ts @@ -77,6 +77,26 @@ async function waitForNewNoteRoute(page: Page, previousNoteId: number | null): P return noteId } +async function createNoteViaApi(page: Page, category: string, title: string): Promise { + return page.evaluate(async ({ category, title }) => { + const requestToken = (window as unknown as { OC: { requestToken: string } }).OC.requestToken + const response = await fetch('/index.php/apps/notes/notes', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + requesttoken: requestToken, + }, + body: JSON.stringify({ category, title, content: '' }), + }) + if (!response.ok) { + throw new Error(`Failed to create note: ${response.status} ${await response.text()}`) + } + + const note = await response.json() as { id: number } + return note.id + }, { category, title }) +} + async function ensureNotesView(page: Page): Promise { if (await notesSearchField(page).isVisible()) { return @@ -154,4 +174,25 @@ test.describe('Category actions', () => { await expect(navigationRow(page, 'All notes')).toHaveClass(/active/) await expect(page).not.toHaveURL(deletedNoteUrl) }) + + test('keeps all notes selected when opening a categorized note', async ({ page }, testInfo: TestInfo) => { + const category = uniqueCategoryName('all-notes', testInfo) + const title = `${category} note` + const noteId = await createNoteViaApi(page, category, title) + + await page.goto('/index.php/apps/notes/') + await expect(contentNewNoteButton(page)).toBeVisible() + await expect(navigationRow(page, category)).toBeVisible() + + await navigationRow(page, 'All notes').click() + await expect(navigationRow(page, 'All notes')).toHaveClass(/active/) + + const noteLink = page.locator(`a[href$="/note/${noteId}"]`).first() + await expect(noteLink).toBeVisible() + await noteLink.click() + + await expect(page).toHaveURL(new RegExp(`/note/${noteId}(\\?.*)?$`)) + await expect(navigationRow(page, 'All notes')).toHaveClass(/active/) + await expect(navigationRow(page, category)).not.toHaveClass(/active/) + }) }) diff --git a/src/Util.js b/src/Util.js index 20f2b4f75..181c6a3d3 100644 --- a/src/Util.js +++ b/src/Util.js @@ -29,6 +29,14 @@ export const categoryLabel = (category) => { return category === '' ? t('notes', 'Uncategorized') : category.replace(/\//g, ' / ') } +export const rootCategory = (category) => { + if (!category) { + return '' + } + const separator = category.indexOf('/') + return separator === -1 ? category : category.substring(0, separator) +} + export const routeIsNewNote = ($route) => { return {}.hasOwnProperty.call($route.query, 'new') } diff --git a/src/components/CategoriesList.vue b/src/components/CategoriesList.vue index 56cef8744..84cf822f1 100644 --- a/src/components/CategoriesList.vue +++ b/src/components/CategoriesList.vue @@ -458,15 +458,29 @@ export default { diff --git a/src/components/NotesView.vue b/src/components/NotesView.vue index df28ff2dd..739ef41fa 100644 --- a/src/components/NotesView.vue +++ b/src/components/NotesView.vue @@ -71,7 +71,7 @@ import NcAppContentList from '@nextcloud/vue/components/NcAppContentList' import NcAppContentDetails from '@nextcloud/vue/components/NcAppContentDetails' import NcButton from '@nextcloud/vue/components/NcButton' import NcTextField from '@nextcloud/vue/components/NcTextField' -import { categoryLabel } from '../Util.js' +import { categoryLabel, rootCategory } from '../Util.js' import NotesList from './NotesList.vue' import NotesCaption from './NotesCaption.vue' import store from '../store.js' @@ -128,6 +128,15 @@ export default { return store.getters.getSelectedCategory() }, + note() { + const noteId = Number.parseInt(this.noteId, 10) + return Number.isFinite(noteId) ? store.getters.getNote(noteId) : null + }, + + noteCategory() { + return this.note?.category ?? null + }, + filteredNotes() { return store.getters.getFilteredNotes() }, @@ -164,16 +173,70 @@ export default { }, watch: { - category() { this.showFirstNotesOnly = true }, + category() { + this.showFirstNotesOnly = true + this.hideVisibleNoteOutsideSelectedCategory() + }, + noteId() { + this.showNote = true + this.updateVisibleNoteSelection() + }, + noteCategory() { + this.updateVisibleNoteSelection() + }, + showNote() { + this.updateVisibleNoteSelection() + }, searchText(value) { store.commit('updateSearchText', value) }, }, created() { this.updateTimeslots() + this.updateVisibleNoteSelection() setInterval(this.updateTimeslots, 1000 * 60) }, + destroyed() { + this.clearVisibleNoteSelection() + }, + methods: { + clearVisibleNoteSelection() { + if (store.getters.getSelectedNote() !== null) { + store.commit('setSelectedNote', null) + } + }, + + updateVisibleNoteSelection() { + const noteId = Number.parseInt(this.noteId, 10) + if (!this.showNote || !Number.isFinite(noteId)) { + this.clearVisibleNoteSelection() + return + } + + if (store.getters.getSelectedNote() !== noteId) { + store.commit('setSelectedNote', noteId) + } + + if (this.note && store.getters.getSelectedCategory() !== null) { + const category = rootCategory(this.note.category) + if (store.getters.getSelectedCategory() !== category) { + store.commit('setSelectedCategory', category) + } + } + }, + + hideVisibleNoteOutsideSelectedCategory() { + if (!this.showNote || !this.note) { + return + } + + const selectedCategory = store.getters.getSelectedCategory() + if (selectedCategory !== null && selectedCategory !== rootCategory(this.note.category)) { + this.showNote = false + } + }, + updateTimeslots() { const now = new Date() // define the time groups we want to allow