From 3cc0d2e8725f5799559f2efc2b54ae4fb0bb7cdd Mon Sep 17 00:00:00 2001 From: ELHart05 Date: Wed, 12 Aug 2026 03:55:06 +0100 Subject: [PATCH 1/2] fix(viewerAction): replace the history entry when closing the viewer Opening a file replaces the folder's history entry with one that carries `openfile`. Closing pushed a new entry on top of it instead of replacing it, so the entry carrying `openfile` was left behind: going back after closing reopened the file that had just been closed instead of leaving the folder, and further back navigation bounced between the leftover entries. Closing now replaces the current entry, mirroring what opening does. Signed-off-by: ELHart05 --- playwright/e2e/navigation.spec.ts | 31 ++++++++++++++++++++++++++++--- src/files_actions/viewerAction.ts | 6 +++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/playwright/e2e/navigation.spec.ts b/playwright/e2e/navigation.spec.ts index 28cef0336..fc89c5431 100644 --- a/playwright/e2e/navigation.spec.ts +++ b/playwright/e2e/navigation.spec.ts @@ -3,9 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { expect, setupFilesPage, test } from '../support/fixtures.ts' -import { getRowForFile, openFile } from '../support/filesUtils.ts' -import { getViewer } from '../support/viewerUtils.ts' +import { createRandomUser, expect, loginUser, setupFilesPage, test } from '../support/fixtures.ts' +import { getRowForFile, navigateToFolder, openFile } from '../support/filesUtils.ts' +import { getCloseButton, getViewer } from '../support/viewerUtils.ts' +import { createFolder, uploadFile } from '../support/webdav.ts' test.describe('Browser navigation', () => { test('Navigating back to the files overview', async ({ browser }) => { @@ -22,4 +23,28 @@ test.describe('Browser navigation', () => { await page.close() }) + + test('Navigating back after closing leaves the folder the file was opened from', async ({ browser }) => { + const page = await browser.newPage() + const user = await createRandomUser() + await createFolder(user, '/Photos') + await uploadFile(user, 'image.png', 'image/png', '/Photos/image1.png') + await loginUser(page, user) + await page.goto('apps/files') + + await navigateToFolder(page, 'Photos') + await openFile(page, 'image1.png') + await expect(getViewer(page)).toBeVisible() + + await getCloseButton(page).click() + await expect(getViewer(page)).toHaveCount(0) + + // Closing must not stack an extra entry on top of the folder: one step back + // leaves the folder instead of reopening the file that was just closed. + await page.goBack() + await expect(getViewer(page)).toHaveCount(0) + await expect(getRowForFile(page, 'Photos')).toBeVisible() + + await page.close() + }) }) diff --git a/src/files_actions/viewerAction.ts b/src/files_actions/viewerAction.ts index 0ebde4433..eaf54de1b 100644 --- a/src/files_actions/viewerAction.ts +++ b/src/files_actions/viewerAction.ts @@ -69,7 +69,11 @@ async function execAction({ nodes, view, folder }): Promise { const newQuery = { ...window.OCP?.Files?.Router?.query } delete newQuery.openfile delete newQuery.editing - window.OCP?.Files?.Router?.goToRoute(null, window.OCP?.Files?.Router?.params, newQuery) + // Replace rather than push: opening the file already replaced the folder's + // history entry, so closing has to give it back. Pushing leaves the entry + // carrying `openfile` behind, and going back then reopens the file that was + // just closed instead of leaving the folder. + window.OCP?.Files?.Router?.goToRoute(null, window.OCP?.Files?.Router?.params, newQuery, true) } if (window.OCP?.Files?.Router) { From fc7f77e7c692ed2b27253807d70dbbad517fb9dd Mon Sep 17 00:00:00 2001 From: ELHart05 Date: Wed, 12 Aug 2026 17:24:21 +0100 Subject: [PATCH 2/2] chore(assets): Recompile assets Signed-off-by: ELHart05 --- ...B73-WtOY.chunk.mjs => NcActionButton-DQZx0wK-.chunk.mjs} | 4 ++-- ...js.license => NcActionButton-DQZx0wK-.chunk.mjs.license} | 0 ....chunk.mjs.map => NcActionButton-DQZx0wK-.chunk.mjs.map} | 2 +- ...icense => NcActionButton-DQZx0wK-.chunk.mjs.map.license} | 0 ...unk.mjs => NcIconSvgWrapper-Bui9PhAS-B1pJetUd.chunk.mjs} | 4 ++-- ...=> NcIconSvgWrapper-Bui9PhAS-B1pJetUd.chunk.mjs.license} | 0 ...map => NcIconSvgWrapper-Bui9PhAS-B1pJetUd.chunk.mjs.map} | 2 +- ...cIconSvgWrapper-Bui9PhAS-B1pJetUd.chunk.mjs.map.license} | 0 ...cModal-DMQg8Dro.chunk.mjs => NcModal-BXFm5Nhr.chunk.mjs} | 4 ++-- ...chunk.mjs.license => NcModal-BXFm5Nhr.chunk.mjs.license} | 0 ...MQg8Dro.chunk.mjs.map => NcModal-BXFm5Nhr.chunk.mjs.map} | 2 +- ...s.map.license => NcModal-BXFm5Nhr.chunk.mjs.map.license} | 0 js/{index-CkPnOG-1.chunk.mjs => index-BW9ErOeI.chunk.mjs} | 4 ++-- ...1.chunk.mjs.license => index-BW9ErOeI.chunk.mjs.license} | 0 ...-CkPnOG-1.chunk.mjs.map => index-BW9ErOeI.chunk.mjs.map} | 2 +- ...mjs.map.license => index-BW9ErOeI.chunk.mjs.map.license} | 0 ...s-DOqZ_eeK.chunk.mjs => previewUtils-Gu_Yk8IX.chunk.mjs} | 4 ++-- ....mjs.license => previewUtils-Gu_Yk8IX.chunk.mjs.license} | 0 ...eK.chunk.mjs.map => previewUtils-Gu_Yk8IX.chunk.mjs.map} | 2 +- ....license => previewUtils-Gu_Yk8IX.chunk.mjs.map.license} | 0 js/viewer-init.mjs | 6 +++--- js/viewer-main.mjs | 6 +++--- 22 files changed, 21 insertions(+), 21 deletions(-) rename js/{NcActionButton-B73-WtOY.chunk.mjs => NcActionButton-DQZx0wK-.chunk.mjs} (92%) rename js/{NcActionButton-B73-WtOY.chunk.mjs.license => NcActionButton-DQZx0wK-.chunk.mjs.license} (100%) rename js/{NcActionButton-B73-WtOY.chunk.mjs.map => NcActionButton-DQZx0wK-.chunk.mjs.map} (99%) rename js/{NcActionButton-B73-WtOY.chunk.mjs.map.license => NcActionButton-DQZx0wK-.chunk.mjs.map.license} (100%) rename js/{NcIconSvgWrapper-Bui9PhAS-DxNXUZ0w.chunk.mjs => NcIconSvgWrapper-Bui9PhAS-B1pJetUd.chunk.mjs} (94%) rename js/{NcIconSvgWrapper-Bui9PhAS-DxNXUZ0w.chunk.mjs.license => NcIconSvgWrapper-Bui9PhAS-B1pJetUd.chunk.mjs.license} (100%) rename js/{NcIconSvgWrapper-Bui9PhAS-DxNXUZ0w.chunk.mjs.map => NcIconSvgWrapper-Bui9PhAS-B1pJetUd.chunk.mjs.map} (99%) rename js/{NcIconSvgWrapper-Bui9PhAS-DxNXUZ0w.chunk.mjs.map.license => NcIconSvgWrapper-Bui9PhAS-B1pJetUd.chunk.mjs.map.license} (100%) rename js/{NcModal-DMQg8Dro.chunk.mjs => NcModal-BXFm5Nhr.chunk.mjs} (99%) rename js/{NcModal-DMQg8Dro.chunk.mjs.license => NcModal-BXFm5Nhr.chunk.mjs.license} (100%) rename js/{NcModal-DMQg8Dro.chunk.mjs.map => NcModal-BXFm5Nhr.chunk.mjs.map} (99%) rename js/{NcModal-DMQg8Dro.chunk.mjs.map.license => NcModal-BXFm5Nhr.chunk.mjs.map.license} (100%) rename js/{index-CkPnOG-1.chunk.mjs => index-BW9ErOeI.chunk.mjs} (99%) rename js/{index-CkPnOG-1.chunk.mjs.license => index-BW9ErOeI.chunk.mjs.license} (100%) rename js/{index-CkPnOG-1.chunk.mjs.map => index-BW9ErOeI.chunk.mjs.map} (99%) rename js/{index-CkPnOG-1.chunk.mjs.map.license => index-BW9ErOeI.chunk.mjs.map.license} (100%) rename js/{previewUtils-DOqZ_eeK.chunk.mjs => previewUtils-Gu_Yk8IX.chunk.mjs} (99%) rename js/{previewUtils-DOqZ_eeK.chunk.mjs.license => previewUtils-Gu_Yk8IX.chunk.mjs.license} (100%) rename js/{previewUtils-DOqZ_eeK.chunk.mjs.map => previewUtils-Gu_Yk8IX.chunk.mjs.map} (77%) rename js/{previewUtils-DOqZ_eeK.chunk.mjs.map.license => previewUtils-Gu_Yk8IX.chunk.mjs.map.license} (100%) diff --git a/js/NcActionButton-B73-WtOY.chunk.mjs b/js/NcActionButton-DQZx0wK-.chunk.mjs similarity index 92% rename from js/NcActionButton-B73-WtOY.chunk.mjs rename to js/NcActionButton-DQZx0wK-.chunk.mjs index 757d5bfda..700cfacba 100644 --- a/js/NcActionButton-B73-WtOY.chunk.mjs +++ b/js/NcActionButton-DQZx0wK-.chunk.mjs @@ -1,2 +1,2 @@ -import{N as i,m as s,b as a}from"./NcIconSvgWrapper-Bui9PhAS-DxNXUZ0w.chunk.mjs";import{A as n}from"./actionText-BMig9Egt-DrBqWVOB.chunk.mjs";import{n as o}from"./_plugin-vue2_normalizer-DU4iP6Vu-CHYf0Z4t.chunk.mjs";import"./previewUtils-DOqZ_eeK.chunk.mjs";const l={name:"NcActionButton",components:{NcIconSvgWrapper:i},mixins:[n],inject:{isInSemanticMenu:{from:"NcActions:isSemanticMenu",default:!1}},props:{ariaHidden:{type:Boolean,default:null},disabled:{type:Boolean,default:!1},isMenu:{type:Boolean,default:!1},type:{type:String,default:"button",validator:t=>["button","checkbox","radio","reset","submit"].includes(t)},modelValue:{type:[Boolean,String],default:null},value:{type:String,default:null},description:{type:String,default:""}},setup(){return{mdiCheck:a,mdiChevronRight:s}},computed:{isFocusable(){return!this.disabled},isChecked(){return this.type==="radio"&&typeof this.modelValue!="boolean"?this.modelValue===this.value:this.modelValue},nativeType(){return this.type==="submit"||this.type==="reset"?this.type:"button"},buttonAttributes(){const t={};return this.isInSemanticMenu?(t.role="menuitem",this.type==="radio"?(t.role="menuitemradio",t["aria-checked"]=this.isChecked?"true":"false"):(this.type==="checkbox"||this.nativeType==="button"&&this.modelValue!==null)&&(t.role="menuitemcheckbox",t["aria-checked"]=this.modelValue===null?"mixed":this.modelValue?"true":"false")):this.modelValue!==null&&this.nativeType==="button"&&(t["aria-pressed"]=this.modelValue?"true":"false"),t}},methods:{handleClick(t){this.onClick(t),(this.modelValue!==null||this.type!=="button")&&(this.type==="radio"?typeof this.modelValue!="boolean"?this.isChecked||this.$emit("update:modelValue",this.value):this.$emit("update:modelValue",!this.isChecked):this.$emit("update:modelValue",!this.isChecked))}}};var c=function(){var t=this,e=t._self._c;return e("li",{staticClass:"action",class:{"action--disabled":t.disabled},attrs:{role:t.isInSemanticMenu&&"presentation"}},[e("button",t._b({staticClass:"action-button button-vue",class:{"action-button--active":t.isChecked,focusable:t.isFocusable},attrs:{"aria-label":t.ariaLabel,disabled:t.disabled,title:t.title,type:t.nativeType},on:{click:t.handleClick}},"button",t.buttonAttributes,!1),[t._t("icon",function(){return[e("span",{staticClass:"action-button__icon",class:[t.isIconUrl?"action-button__icon--url":t.icon],style:{backgroundImage:t.isIconUrl?`url(${t.icon})`:null},attrs:{"aria-hidden":"true"}})]}),e("span",{staticClass:"action-button__longtext-wrapper"},[t.name?e("strong",{staticClass:"action-button__name"},[t._v(" "+t._s(t.name)+" ")]):t._e(),t.isLongText?e("span",{staticClass:"action-button__longtext",domProps:{textContent:t._s(t.text)}}):e("span",{staticClass:"action-button__text"},[t._v(" "+t._s(t.text)+" ")]),t.description?e("span",{staticClass:"action-button__description",domProps:{textContent:t._s(t.description)}}):t._e()]),t.isMenu?e("NcIconSvgWrapper",{staticClass:"action-button__menu-icon",attrs:{directional:"",path:t.mdiChevronRight}}):t.isChecked?e("NcIconSvgWrapper",{staticClass:"action-button__pressed-icon",attrs:{path:t.mdiCheck}}):t.isChecked===!1?e("span",{staticClass:"action-button__pressed-icon material-design-icon"}):t._e(),t._e()],2)])},u=[],r=o(l,c,u,!1,null,"ab2ff78b");const b=r.exports;export{b as default}; -//# sourceMappingURL=NcActionButton-B73-WtOY.chunk.mjs.map +import{N as i,m as s,b as a}from"./NcIconSvgWrapper-Bui9PhAS-B1pJetUd.chunk.mjs";import{A as n}from"./actionText-BMig9Egt-DrBqWVOB.chunk.mjs";import{n as o}from"./_plugin-vue2_normalizer-DU4iP6Vu-CHYf0Z4t.chunk.mjs";import"./previewUtils-Gu_Yk8IX.chunk.mjs";const l={name:"NcActionButton",components:{NcIconSvgWrapper:i},mixins:[n],inject:{isInSemanticMenu:{from:"NcActions:isSemanticMenu",default:!1}},props:{ariaHidden:{type:Boolean,default:null},disabled:{type:Boolean,default:!1},isMenu:{type:Boolean,default:!1},type:{type:String,default:"button",validator:t=>["button","checkbox","radio","reset","submit"].includes(t)},modelValue:{type:[Boolean,String],default:null},value:{type:String,default:null},description:{type:String,default:""}},setup(){return{mdiCheck:a,mdiChevronRight:s}},computed:{isFocusable(){return!this.disabled},isChecked(){return this.type==="radio"&&typeof this.modelValue!="boolean"?this.modelValue===this.value:this.modelValue},nativeType(){return this.type==="submit"||this.type==="reset"?this.type:"button"},buttonAttributes(){const t={};return this.isInSemanticMenu?(t.role="menuitem",this.type==="radio"?(t.role="menuitemradio",t["aria-checked"]=this.isChecked?"true":"false"):(this.type==="checkbox"||this.nativeType==="button"&&this.modelValue!==null)&&(t.role="menuitemcheckbox",t["aria-checked"]=this.modelValue===null?"mixed":this.modelValue?"true":"false")):this.modelValue!==null&&this.nativeType==="button"&&(t["aria-pressed"]=this.modelValue?"true":"false"),t}},methods:{handleClick(t){this.onClick(t),(this.modelValue!==null||this.type!=="button")&&(this.type==="radio"?typeof this.modelValue!="boolean"?this.isChecked||this.$emit("update:modelValue",this.value):this.$emit("update:modelValue",!this.isChecked):this.$emit("update:modelValue",!this.isChecked))}}};var c=function(){var t=this,e=t._self._c;return e("li",{staticClass:"action",class:{"action--disabled":t.disabled},attrs:{role:t.isInSemanticMenu&&"presentation"}},[e("button",t._b({staticClass:"action-button button-vue",class:{"action-button--active":t.isChecked,focusable:t.isFocusable},attrs:{"aria-label":t.ariaLabel,disabled:t.disabled,title:t.title,type:t.nativeType},on:{click:t.handleClick}},"button",t.buttonAttributes,!1),[t._t("icon",function(){return[e("span",{staticClass:"action-button__icon",class:[t.isIconUrl?"action-button__icon--url":t.icon],style:{backgroundImage:t.isIconUrl?`url(${t.icon})`:null},attrs:{"aria-hidden":"true"}})]}),e("span",{staticClass:"action-button__longtext-wrapper"},[t.name?e("strong",{staticClass:"action-button__name"},[t._v(" "+t._s(t.name)+" ")]):t._e(),t.isLongText?e("span",{staticClass:"action-button__longtext",domProps:{textContent:t._s(t.text)}}):e("span",{staticClass:"action-button__text"},[t._v(" "+t._s(t.text)+" ")]),t.description?e("span",{staticClass:"action-button__description",domProps:{textContent:t._s(t.description)}}):t._e()]),t.isMenu?e("NcIconSvgWrapper",{staticClass:"action-button__menu-icon",attrs:{directional:"",path:t.mdiChevronRight}}):t.isChecked?e("NcIconSvgWrapper",{staticClass:"action-button__pressed-icon",attrs:{path:t.mdiCheck}}):t.isChecked===!1?e("span",{staticClass:"action-button__pressed-icon material-design-icon"}):t._e(),t._e()],2)])},u=[],r=o(l,c,u,!1,null,"ab2ff78b");const b=r.exports;export{b as default}; +//# sourceMappingURL=NcActionButton-DQZx0wK-.chunk.mjs.map diff --git a/js/NcActionButton-B73-WtOY.chunk.mjs.license b/js/NcActionButton-DQZx0wK-.chunk.mjs.license similarity index 100% rename from js/NcActionButton-B73-WtOY.chunk.mjs.license rename to js/NcActionButton-DQZx0wK-.chunk.mjs.license diff --git a/js/NcActionButton-B73-WtOY.chunk.mjs.map b/js/NcActionButton-DQZx0wK-.chunk.mjs.map similarity index 99% rename from js/NcActionButton-B73-WtOY.chunk.mjs.map rename to js/NcActionButton-DQZx0wK-.chunk.mjs.map index 9962e1364..dc706dbeb 100644 --- a/js/NcActionButton-B73-WtOY.chunk.mjs.map +++ b/js/NcActionButton-DQZx0wK-.chunk.mjs.map @@ -1 +1 @@ -{"version":3,"file":"NcActionButton-B73-WtOY.chunk.mjs","sources":["../node_modules/@nextcloud/vue/dist/chunks/NcActionButton-D7uypboC.mjs"],"sourcesContent":["import '../assets/NcActionButton-CwGeOQFe.css';\nimport { m as mdiChevronRight, a as mdiCheck } from \"./mdi-DkJglNiS.mjs\";\nimport { N as NcIconSvgWrapper } from \"./NcIconSvgWrapper-Bui9PhAS.mjs\";\nimport { A as ActionTextMixin } from \"./actionText-BMig9Egt.mjs\";\nimport { n as normalizeComponent } from \"./_plugin-vue2_normalizer-DU4iP6Vu.mjs\";\nconst _sfc_main = {\n name: \"NcActionButton\",\n components: {\n NcIconSvgWrapper\n },\n mixins: [ActionTextMixin],\n inject: {\n isInSemanticMenu: {\n from: \"NcActions:isSemanticMenu\",\n default: false\n }\n },\n props: {\n /**\n * @deprecated To be removed in @nextcloud/vue 9. Migration guide: remove ariaHidden prop from NcAction* components.\n * @todo Add a check in @nextcloud/vue 9 that this prop is not provided,\n * otherwise root element will inherit incorrect aria-hidden.\n */\n ariaHidden: {\n type: Boolean,\n // eslint-disable-next-line vue/no-boolean-default\n default: null\n },\n /**\n * disabled state of the action button\n */\n disabled: {\n type: Boolean,\n default: false\n },\n /**\n * If this is a menu, a chevron icon will\n * be added at the end of the line\n */\n isMenu: {\n type: Boolean,\n default: false\n },\n /**\n * The button's behavior, by default the button acts like a normal button with optional toggle button behavior if `modelValue` is `true` or `false`.\n * But you can also set to checkbox button behavior with tri-state or radio button like behavior.\n * This extends the native HTML button type attribute.\n */\n type: {\n type: String,\n default: \"button\",\n validator: (behavior) => [\"button\", \"checkbox\", \"radio\", \"reset\", \"submit\"].includes(behavior)\n },\n /**\n * The buttons state if `type` is 'checkbox' or 'radio' (meaning if it is pressed / selected).\n * For checkbox and toggle button behavior - boolean value.\n * For radio button behavior - could be a boolean checked or a string with the value of the button.\n * Note: Unlike native radio buttons, NcActionButton are not grouped by name, so you need to connect them by bind correct modelValue.\n *\n * **This is not availabe for `type='submit'` or `type='reset'`**\n *\n * If using `type='checkbox'` a `model-value` of `true` means checked, `false` means unchecked and `null` means indeterminate (tri-state)\n * For `type='radio'` `null` is equal to `false`\n */\n modelValue: {\n type: [Boolean, String],\n default: null\n },\n /**\n * The value used for the `modelValue` when this component is used with radio behavior\n * Similar to the `value` attribute of ``\n */\n value: {\n type: String,\n default: null\n },\n /**\n * Small underlying text content of the entry\n */\n description: {\n type: String,\n default: \"\"\n }\n },\n setup() {\n return {\n mdiCheck,\n mdiChevronRight\n };\n },\n computed: {\n /**\n * determines if the action is focusable\n *\n * @return {boolean} is the action focusable ?\n */\n isFocusable() {\n return !this.disabled;\n },\n /**\n * The current \"checked\" or \"pressed\" state for the model behavior\n */\n isChecked() {\n if (this.type === \"radio\" && typeof this.modelValue !== \"boolean\") {\n return this.modelValue === this.value;\n }\n return this.modelValue;\n },\n /**\n * The native HTML type to set on the button\n */\n nativeType() {\n if (this.type === \"submit\" || this.type === \"reset\") {\n return this.type;\n }\n return \"button\";\n },\n /**\n * HTML attributes to bind to the