From d850ea59526173fb22f98dff6270a20db038afaf 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 (cherry picked from commit a867a5c1f6c3efaf073d4d59d39c43ff4d31b99f) Signed-off-by: skjnldsv --- cypress/e2e/navigation.cy.ts | 20 ++++++++++++++++++++ src/files_actions/viewerAction.ts | 6 +++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/navigation.cy.ts b/cypress/e2e/navigation.cy.ts index f5dc1d3c6..5ae3f978a 100644 --- a/cypress/e2e/navigation.cy.ts +++ b/cypress/e2e/navigation.cy.ts @@ -9,6 +9,8 @@ describe('Browser navigation', function() { cy.createRandomUser().then(user => { // Upload test files cy.uploadFile(user, 'image.png', 'image/png', '/image1.png') + cy.createFolder(user, '/Photos') + cy.uploadFile(user, 'image.png', 'image/png', '/Photos/image1.png') // Visit nextcloud cy.login(user) @@ -26,4 +28,22 @@ describe('Browser navigation', function() { cy.go('back') cy.get('body > .viewer').should('not.exist') }) + + it('Navigating back after closing leaves the folder the file was opened from', function() { + cy.visit('/apps/files') + cy.getFile('Photos', { timeout: 10000 }) + cy.openFile('Photos') + cy.getFile('image1.png', { timeout: 10000 }) + cy.openFile('image1.png') + cy.get('body > .viewer').should('be.visible') + + cy.get('body > .viewer button.header-close').click() + cy.get('body > .viewer').should('not.exist') + + // Closing must not stack an entry on top of the folder's: one step back + // leaves the folder, rather than reopening the file that was just closed + cy.go('back') + cy.get('body > .viewer').should('not.exist') + cy.getFile('Photos').should('be.visible') + }) }) 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 26641df5142b33fe9902f57b37a5c5383e8fe997 Mon Sep 17 00:00:00 2001 From: nextcloud-command Date: Fri, 18 Sep 2026 12:15:53 +0000 Subject: [PATCH 2/2] chore(assets): Recompile assets Signed-off-by: nextcloud-command --- ...cV_XUd.chunk.mjs => NcActionButton-3TNS9lSP.chunk.mjs} | 4 ++-- ....license => NcActionButton-3TNS9lSP.chunk.mjs.license} | 0 ...hunk.mjs.map => NcActionButton-3TNS9lSP.chunk.mjs.map} | 2 +- ...ense => NcActionButton-3TNS9lSP.chunk.mjs.map.license} | 0 ...k.mjs => NcIconSvgWrapper-Bui9PhAS-PtDRF55t.chunk.mjs} | 4 ++-- ... NcIconSvgWrapper-Bui9PhAS-PtDRF55t.chunk.mjs.license} | 0 ...p => NcIconSvgWrapper-Bui9PhAS-PtDRF55t.chunk.mjs.map} | 2 +- ...conSvgWrapper-Bui9PhAS-PtDRF55t.chunk.mjs.map.license} | 0 ...odal-BZnTeZbt.chunk.mjs => NcModal-CvtBvir7.chunk.mjs} | 4 ++-- ...unk.mjs.license => NcModal-CvtBvir7.chunk.mjs.license} | 0 ...TeZbt.chunk.mjs.map => NcModal-CvtBvir7.chunk.mjs.map} | 2 +- ...map.license => NcModal-CvtBvir7.chunk.mjs.map.license} | 0 js/{index-BYb1yJc2.chunk.mjs => index-DbkfmI_z.chunk.mjs} | 4 ++-- ...chunk.mjs.license => index-DbkfmI_z.chunk.mjs.license} | 0 ...Yb1yJc2.chunk.mjs.map => index-DbkfmI_z.chunk.mjs.map} | 2 +- ...s.map.license => index-DbkfmI_z.chunk.mjs.map.license} | 0 js/{index-hj8-RJzt.chunk.mjs => index-jKxBmN_i.chunk.mjs} | 4 ++-- ...chunk.mjs.license => index-jKxBmN_i.chunk.mjs.license} | 0 ...j8-RJzt.chunk.mjs.map => index-jKxBmN_i.chunk.mjs.map} | 2 +- ...s.map.license => index-jKxBmN_i.chunk.mjs.map.license} | 0 ...B0UPOBK0.chunk.mjs => previewUtils-C1kPIckI.chunk.mjs} | 4 ++-- ...js.license => previewUtils-C1kPIckI.chunk.mjs.license} | 0 ....chunk.mjs.map => previewUtils-C1kPIckI.chunk.mjs.map} | 2 +- ...icense => previewUtils-C1kPIckI.chunk.mjs.map.license} | 0 js/viewer-init.mjs | 8 ++++---- js/viewer-main.mjs | 6 +++--- 26 files changed, 25 insertions(+), 25 deletions(-) rename js/{NcActionButton-MDcV_XUd.chunk.mjs => NcActionButton-3TNS9lSP.chunk.mjs} (92%) rename js/{NcActionButton-MDcV_XUd.chunk.mjs.license => NcActionButton-3TNS9lSP.chunk.mjs.license} (100%) rename js/{NcActionButton-MDcV_XUd.chunk.mjs.map => NcActionButton-3TNS9lSP.chunk.mjs.map} (99%) rename js/{NcActionButton-MDcV_XUd.chunk.mjs.map.license => NcActionButton-3TNS9lSP.chunk.mjs.map.license} (100%) rename js/{NcIconSvgWrapper-Bui9PhAS-DojgibU_.chunk.mjs => NcIconSvgWrapper-Bui9PhAS-PtDRF55t.chunk.mjs} (94%) rename js/{NcIconSvgWrapper-Bui9PhAS-DojgibU_.chunk.mjs.license => NcIconSvgWrapper-Bui9PhAS-PtDRF55t.chunk.mjs.license} (100%) rename js/{NcIconSvgWrapper-Bui9PhAS-DojgibU_.chunk.mjs.map => NcIconSvgWrapper-Bui9PhAS-PtDRF55t.chunk.mjs.map} (99%) rename js/{NcIconSvgWrapper-Bui9PhAS-DojgibU_.chunk.mjs.map.license => NcIconSvgWrapper-Bui9PhAS-PtDRF55t.chunk.mjs.map.license} (100%) rename js/{NcModal-BZnTeZbt.chunk.mjs => NcModal-CvtBvir7.chunk.mjs} (99%) rename js/{NcModal-BZnTeZbt.chunk.mjs.license => NcModal-CvtBvir7.chunk.mjs.license} (100%) rename js/{NcModal-BZnTeZbt.chunk.mjs.map => NcModal-CvtBvir7.chunk.mjs.map} (99%) rename js/{NcModal-BZnTeZbt.chunk.mjs.map.license => NcModal-CvtBvir7.chunk.mjs.map.license} (100%) rename js/{index-BYb1yJc2.chunk.mjs => index-DbkfmI_z.chunk.mjs} (99%) rename js/{index-BYb1yJc2.chunk.mjs.license => index-DbkfmI_z.chunk.mjs.license} (100%) rename js/{index-BYb1yJc2.chunk.mjs.map => index-DbkfmI_z.chunk.mjs.map} (99%) rename js/{index-BYb1yJc2.chunk.mjs.map.license => index-DbkfmI_z.chunk.mjs.map.license} (100%) rename js/{index-hj8-RJzt.chunk.mjs => index-jKxBmN_i.chunk.mjs} (99%) rename js/{index-hj8-RJzt.chunk.mjs.license => index-jKxBmN_i.chunk.mjs.license} (100%) rename js/{index-hj8-RJzt.chunk.mjs.map => index-jKxBmN_i.chunk.mjs.map} (99%) rename js/{index-hj8-RJzt.chunk.mjs.map.license => index-jKxBmN_i.chunk.mjs.map.license} (100%) rename js/{previewUtils-B0UPOBK0.chunk.mjs => previewUtils-C1kPIckI.chunk.mjs} (99%) rename js/{previewUtils-B0UPOBK0.chunk.mjs.license => previewUtils-C1kPIckI.chunk.mjs.license} (100%) rename js/{previewUtils-B0UPOBK0.chunk.mjs.map => previewUtils-C1kPIckI.chunk.mjs.map} (78%) rename js/{previewUtils-B0UPOBK0.chunk.mjs.map.license => previewUtils-C1kPIckI.chunk.mjs.map.license} (100%) diff --git a/js/NcActionButton-MDcV_XUd.chunk.mjs b/js/NcActionButton-3TNS9lSP.chunk.mjs similarity index 92% rename from js/NcActionButton-MDcV_XUd.chunk.mjs rename to js/NcActionButton-3TNS9lSP.chunk.mjs index 19cb19e94..fd5b82f75 100644 --- a/js/NcActionButton-MDcV_XUd.chunk.mjs +++ b/js/NcActionButton-3TNS9lSP.chunk.mjs @@ -1,2 +1,2 @@ -import{N as i,m as s,b as a}from"./NcIconSvgWrapper-Bui9PhAS-DojgibU_.chunk.mjs";import{A as n}from"./actionText-BMig9Egt-DrBqWVOB.chunk.mjs";import{n as o}from"./_plugin-vue2_normalizer-DU4iP6Vu-UEw9p-J1.chunk.mjs";import"./previewUtils-B0UPOBK0.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-MDcV_XUd.chunk.mjs.map +import{N as i,m as s,b as a}from"./NcIconSvgWrapper-Bui9PhAS-PtDRF55t.chunk.mjs";import{A as n}from"./actionText-BMig9Egt-DrBqWVOB.chunk.mjs";import{n as o}from"./_plugin-vue2_normalizer-DU4iP6Vu-UEw9p-J1.chunk.mjs";import"./previewUtils-C1kPIckI.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-3TNS9lSP.chunk.mjs.map diff --git a/js/NcActionButton-MDcV_XUd.chunk.mjs.license b/js/NcActionButton-3TNS9lSP.chunk.mjs.license similarity index 100% rename from js/NcActionButton-MDcV_XUd.chunk.mjs.license rename to js/NcActionButton-3TNS9lSP.chunk.mjs.license diff --git a/js/NcActionButton-MDcV_XUd.chunk.mjs.map b/js/NcActionButton-3TNS9lSP.chunk.mjs.map similarity index 99% rename from js/NcActionButton-MDcV_XUd.chunk.mjs.map rename to js/NcActionButton-3TNS9lSP.chunk.mjs.map index 7cec1dee3..25f34e97a 100644 --- a/js/NcActionButton-MDcV_XUd.chunk.mjs.map +++ b/js/NcActionButton-3TNS9lSP.chunk.mjs.map @@ -1 +1 @@ -{"version":3,"file":"NcActionButton-MDcV_XUd.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