From d94807ae2bd5e28afa5e650de4d12b90db48c3d0 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 24 Aug 2026 10:21:10 -0500 Subject: [PATCH 1/5] GitHub Issue #899: Save grid view fix for saving/resolving inherited view when target folder provided --- .../query/controllers/QueryController.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index 8603332e28b..3c86993a1e9 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -2558,6 +2558,12 @@ protected JSONObject saveCustomView(Container container, QueryDefinition queryDe else view = queryDef.getCustomView(owner, getViewContext().getRequest(), name); + // GitHub Issue #899: the lookups above also resolve views inherited from ancestor folders. containerPath is only + // honored when inherit is set, so otherwise shadow that view with a new local one rather than editing (and + // relocating) the ancestor's. + if (view != null && !inherit && view.getContainer() != null && !container.equals(view.getContainer())) + view = null; + if (view != null && !replaceExisting && !StringUtils.isEmpty(name)) errors.reject(ERROR_MSG, "A saved view by the name \"" + viewName + "\" already exists. "); @@ -6220,8 +6226,9 @@ public ApiResponse execute(SaveSessionViewForm form, BindException errors) // Users may save views to a location other than the current container String containerPath = form.getContainerPath(); + boolean explicitTargetContainer = form.isInherit() && containerPath != null; Container container; - if (form.isInherit() && containerPath != null) + if (explicitTargetContainer) { // Only respect this request if it's a view that is inheritable in subfolders container = ContainerManager.getForPath(containerPath); @@ -6259,6 +6266,14 @@ public ApiResponse execute(SaveSessionViewForm form, BindException errors) existingView = form.getQueryDef().getCustomView(getUser(), null, form.getNewName()); } + // GitHub Issue #899: getCustomView() also resolves views inherited from ancestor folders. Absent an explicit + // target folder, shadow that view with a new local one instead of rewriting (and un-inheriting) the ancestor's. + if (existingView != null && !explicitTargetContainer && existingView.getContainer() != null + && !container.equals(existingView.getContainer())) + { + existingView = null; + } + // save a new private view if shared is false but existing view is shared if (existingView != null && !form.isShared() && existingView.getOwner() == null) { @@ -6278,8 +6293,7 @@ public ApiResponse execute(SaveSessionViewForm form, BindException errors) viewCopy.setFilterAndSort(view.getFilterAndSort()); viewCopy.setColumnProperties(view.getColumnProperties()); viewCopy.setIsHidden(form.isHidden()); - if (form.isInherit()) - viewCopy.setContainer(container); + viewCopy.setContainer(container); viewCopy.save(getUser(), getViewContext().getRequest()); } From b8cde4a9125b54b21303fbbbed4f249b1f40b1af Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 24 Aug 2026 13:08:54 -0500 Subject: [PATCH 2/5] GitHub Issue #899: Save grid view fix for saving/resolving inherited view when target folder provided --- .../query/controllers/QueryController.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index 3c86993a1e9..f5892585d5a 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -2534,7 +2534,7 @@ public void addNavTrail(NavTree root) // Uck. Supports the old and new view designer. protected JSONObject saveCustomView(Container container, QueryDefinition queryDef, String regionName, String viewName, boolean replaceExisting, - boolean share, boolean inherit, + boolean share, boolean inherit, boolean explicitTargetContainer, boolean session, boolean saveFilter, boolean hidden, JSONObject jsonView, ActionURL returnUrl, @@ -2558,10 +2558,9 @@ protected JSONObject saveCustomView(Container container, QueryDefinition queryDe else view = queryDef.getCustomView(owner, getViewContext().getRequest(), name); - // GitHub Issue #899: the lookups above also resolve views inherited from ancestor folders. containerPath is only - // honored when inherit is set, so otherwise shadow that view with a new local one rather than editing (and - // relocating) the ancestor's. - if (view != null && !inherit && view.getContainer() != null && !container.equals(view.getContainer())) + // GitHub Issue #899: the lookups above also resolve views inherited from ancestor folders. Absent an explicit + // target folder, shadow that view with a new local one rather than editing (and relocating) the ancestor's. + if (view != null && !explicitTargetContainer && view.getContainer() != null && !container.equals(view.getContainer())) view = null; if (view != null && !replaceExisting && !StringUtils.isEmpty(name)) @@ -2631,7 +2630,7 @@ else if (session != view.isSession()) try { view.delete(getUser(), getViewContext().getRequest()); - JSONObject ret = saveCustomView(container, queryDef, regionName, viewName, replaceExisting, share, inherit, session, saveFilter, hidden, jsonView, returnUrl, errors); + JSONObject ret = saveCustomView(container, queryDef, regionName, viewName, replaceExisting, share, inherit, explicitTargetContainer, session, saveFilter, hidden, jsonView, returnUrl, errors); success = !errors.hasErrors() && ret != null; return success ? ret : null; } @@ -2776,9 +2775,10 @@ public ApiResponse execute(SimpleApiJsonForm form, BindException errors) boolean session = jsonView.optBoolean("session", false); boolean hidden = jsonView.optBoolean("hidden", false); // Users may save views to a location other than the current container - String containerPath = jsonView.optString("containerPath", getContainer().getPath()); + String containerPath = jsonView.optString("containerPath", null); + boolean explicitTargetContainer = inherit && containerPath != null; Container container; - if (inherit) + if (explicitTargetContainer) { // Only respect this request if it's a view that is inheritable in subfolders container = ContainerManager.getForPath(containerPath); @@ -2796,7 +2796,7 @@ public ApiResponse execute(SimpleApiJsonForm form, BindException errors) JSONObject savedView = saveCustomView( container, queryDef, QueryView.DATAREGIONNAME_DEFAULT, viewName, replace, - shared, inherit, session, true, hidden, jsonView, null, errors); + shared, inherit, explicitTargetContainer, session, true, hidden, jsonView, null, errors); if (savedView != null) { From 5d1f7848e8d523fece85f35e42ae4a6225913e07 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 24 Aug 2026 15:10:28 -0500 Subject: [PATCH 3/5] Add back duplicate view name check for inherited case --- .../org/labkey/query/controllers/QueryController.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index f5892585d5a..f1f6529e3e2 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -2560,11 +2560,17 @@ protected JSONObject saveCustomView(Container container, QueryDefinition queryDe // GitHub Issue #899: the lookups above also resolve views inherited from ancestor folders. Absent an explicit // target folder, shadow that view with a new local one rather than editing (and relocating) the ancestor's. + CustomView inheritedView = null; if (view != null && !explicitTargetContainer && view.getContainer() != null && !container.equals(view.getContainer())) + { + inheritedView = view; view = null; + } if (view != null && !replaceExisting && !StringUtils.isEmpty(name)) errors.reject(ERROR_MSG, "A saved view by the name \"" + viewName + "\" already exists. "); + else if (inheritedView != null && !replaceExisting && !StringUtils.isEmpty(name)) + errors.reject(ERROR_MSG, "A saved view by the name \"" + viewName + "\" is already inherited from folder \"" + inheritedView.getContainer().getPath() + "\". "); // 11179: Allow editing the view if we're saving to session. // NOTE: Check for session flag first otherwise the call to canEdit() will add errors to the errors collection. @@ -6268,12 +6274,17 @@ public ApiResponse execute(SaveSessionViewForm form, BindException errors) // GitHub Issue #899: getCustomView() also resolves views inherited from ancestor folders. Absent an explicit // target folder, shadow that view with a new local one instead of rewriting (and un-inheriting) the ancestor's. + CustomView inheritedView = null; if (existingView != null && !explicitTargetContainer && existingView.getContainer() != null && !container.equals(existingView.getContainer())) { + inheritedView = existingView; existingView = null; } + if (inheritedView != null && !form.isReplace() && !StringUtils.isEmpty(form.getNewName())) + throw new IllegalArgumentException("A saved view by the name \"" + form.getNewName() + "\" is already inherited from folder \"" + inheritedView.getContainer().getPath() + "\". "); + // save a new private view if shared is false but existing view is shared if (existingView != null && !form.isShared() && existingView.getOwner() == null) { From fa0b8ede9280a65d77bc4b84986ffae90383b4c6 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 24 Aug 2026 15:17:20 -0500 Subject: [PATCH 4/5] update @labkey/components package version --- assay/package-lock.json | 8 ++++---- assay/package.json | 2 +- core/package-lock.json | 8 ++++---- core/package.json | 2 +- experiment/package-lock.json | 8 ++++---- experiment/package.json | 2 +- pipeline/package-lock.json | 8 ++++---- pipeline/package.json | 2 +- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/assay/package-lock.json b/assay/package-lock.json index a64072f2048..75ee93f5a7a 100644 --- a/assay/package-lock.json +++ b/assay/package-lock.json @@ -8,7 +8,7 @@ "name": "assay", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.58.1" + "@labkey/components": "7.58.6-fb-saveView899.1" }, "devDependencies": { "@labkey/build": "10.1.2", @@ -2366,9 +2366,9 @@ } }, "node_modules/@labkey/components": { - "version": "7.58.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.58.1.tgz", - "integrity": "sha512-bgsETI9c7mOAY8/1sxoEWK8py40+YF4pUOxcWU2PdSObPHS/Ncv66nZCvB7l7GNDfghQnD4E8x01Ze6lvpoMmQ==", + "version": "7.58.6-fb-saveView899.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.58.6-fb-saveView899.1.tgz", + "integrity": "sha512-IyBLwHY7Z6X0QgWokop5dO/zLMe0xA86TZA5s+X5NTQf/zWjmZWvD3R/oq/9d3C74MxkIzv1Z0sSK0P9/JjsLQ==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/assay/package.json b/assay/package.json index 1a5c30a95c5..ddc707c0d7d 100644 --- a/assay/package.json +++ b/assay/package.json @@ -15,7 +15,7 @@ "lint-fix": "eslint --fix" }, "dependencies": { - "@labkey/components": "7.58.1" + "@labkey/components": "7.58.6-fb-saveView899.1" }, "devDependencies": { "@labkey/build": "10.1.2", diff --git a/core/package-lock.json b/core/package-lock.json index aabeeddc3d2..a768c70ff49 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -8,7 +8,7 @@ "name": "labkey-core", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.58.1", + "@labkey/components": "7.58.6-fb-saveView899.1", "@labkey/themes": "1.9.5" }, "devDependencies": { @@ -2369,9 +2369,9 @@ } }, "node_modules/@labkey/components": { - "version": "7.58.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.58.1.tgz", - "integrity": "sha512-bgsETI9c7mOAY8/1sxoEWK8py40+YF4pUOxcWU2PdSObPHS/Ncv66nZCvB7l7GNDfghQnD4E8x01Ze6lvpoMmQ==", + "version": "7.58.6-fb-saveView899.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.58.6-fb-saveView899.1.tgz", + "integrity": "sha512-IyBLwHY7Z6X0QgWokop5dO/zLMe0xA86TZA5s+X5NTQf/zWjmZWvD3R/oq/9d3C74MxkIzv1Z0sSK0P9/JjsLQ==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/core/package.json b/core/package.json index 717a60c080e..5655cdf52e7 100644 --- a/core/package.json +++ b/core/package.json @@ -20,7 +20,7 @@ "lint-branch-fix": "node lint.diff.mjs --currentBranch --fix" }, "dependencies": { - "@labkey/components": "7.58.1", + "@labkey/components": "7.58.6-fb-saveView899.1", "@labkey/themes": "1.9.5" }, "devDependencies": { diff --git a/experiment/package-lock.json b/experiment/package-lock.json index 35a8dd69da5..439cb0aae34 100644 --- a/experiment/package-lock.json +++ b/experiment/package-lock.json @@ -8,7 +8,7 @@ "name": "experiment", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.58.1" + "@labkey/components": "7.58.6-fb-saveView899.1" }, "devDependencies": { "@labkey/build": "10.1.2", @@ -2378,9 +2378,9 @@ } }, "node_modules/@labkey/components": { - "version": "7.58.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.58.1.tgz", - "integrity": "sha512-bgsETI9c7mOAY8/1sxoEWK8py40+YF4pUOxcWU2PdSObPHS/Ncv66nZCvB7l7GNDfghQnD4E8x01Ze6lvpoMmQ==", + "version": "7.58.6-fb-saveView899.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.58.6-fb-saveView899.1.tgz", + "integrity": "sha512-IyBLwHY7Z6X0QgWokop5dO/zLMe0xA86TZA5s+X5NTQf/zWjmZWvD3R/oq/9d3C74MxkIzv1Z0sSK0P9/JjsLQ==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/experiment/package.json b/experiment/package.json index be0823caceb..3c5444bd27d 100644 --- a/experiment/package.json +++ b/experiment/package.json @@ -13,7 +13,7 @@ "test-integration": "cross-env NODE_ENV=test jest --ci --runInBand -c test/js/jest.config.integration.js" }, "dependencies": { - "@labkey/components": "7.58.1" + "@labkey/components": "7.58.6-fb-saveView899.1" }, "devDependencies": { "@labkey/build": "10.1.2", diff --git a/pipeline/package-lock.json b/pipeline/package-lock.json index 062c30b8df0..9450a5f207e 100644 --- a/pipeline/package-lock.json +++ b/pipeline/package-lock.json @@ -8,7 +8,7 @@ "name": "pipeline", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.58.1" + "@labkey/components": "7.58.6-fb-saveView899.1" }, "devDependencies": { "@labkey/build": "10.1.2", @@ -1469,9 +1469,9 @@ } }, "node_modules/@labkey/components": { - "version": "7.58.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.58.1.tgz", - "integrity": "sha512-bgsETI9c7mOAY8/1sxoEWK8py40+YF4pUOxcWU2PdSObPHS/Ncv66nZCvB7l7GNDfghQnD4E8x01Ze6lvpoMmQ==", + "version": "7.58.6-fb-saveView899.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.58.6-fb-saveView899.1.tgz", + "integrity": "sha512-IyBLwHY7Z6X0QgWokop5dO/zLMe0xA86TZA5s+X5NTQf/zWjmZWvD3R/oq/9d3C74MxkIzv1Z0sSK0P9/JjsLQ==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/pipeline/package.json b/pipeline/package.json index f2b575a4bf8..2cc10e52464 100644 --- a/pipeline/package.json +++ b/pipeline/package.json @@ -14,7 +14,7 @@ "build-prod": "npm run clean && cross-env NODE_ENV=production rspack build --config node_modules/@labkey/build/configs/prod.config.js" }, "dependencies": { - "@labkey/components": "7.58.1" + "@labkey/components": "7.58.6-fb-saveView899.1" }, "devDependencies": { "@labkey/build": "10.1.2", From ce135cea7f40f829b3f6715ea8097d2ffabad46d Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 25 Aug 2026 09:15:57 -0500 Subject: [PATCH 5/5] Claude CR - rename explicitTargetContainer to inheritToTargetContainer, add EditSharedViewPermission container check, move "save a new private view" check up --- .../query/controllers/QueryController.java | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index f1f6529e3e2..88c9261ef4c 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -2534,7 +2534,7 @@ public void addNavTrail(NavTree root) // Uck. Supports the old and new view designer. protected JSONObject saveCustomView(Container container, QueryDefinition queryDef, String regionName, String viewName, boolean replaceExisting, - boolean share, boolean inherit, boolean explicitTargetContainer, + boolean share, boolean inherit, boolean inheritToTargetContainer, boolean session, boolean saveFilter, boolean hidden, JSONObject jsonView, ActionURL returnUrl, @@ -2561,7 +2561,7 @@ protected JSONObject saveCustomView(Container container, QueryDefinition queryDe // GitHub Issue #899: the lookups above also resolve views inherited from ancestor folders. Absent an explicit // target folder, shadow that view with a new local one rather than editing (and relocating) the ancestor's. CustomView inheritedView = null; - if (view != null && !explicitTargetContainer && view.getContainer() != null && !container.equals(view.getContainer())) + if (view != null && !inheritToTargetContainer && view.getContainer() != null && !container.equals(view.getContainer())) { inheritedView = view; view = null; @@ -2569,7 +2569,7 @@ protected JSONObject saveCustomView(Container container, QueryDefinition queryDe if (view != null && !replaceExisting && !StringUtils.isEmpty(name)) errors.reject(ERROR_MSG, "A saved view by the name \"" + viewName + "\" already exists. "); - else if (inheritedView != null && !replaceExisting && !StringUtils.isEmpty(name)) + if (inheritedView != null && !replaceExisting && !StringUtils.isEmpty(name)) errors.reject(ERROR_MSG, "A saved view by the name \"" + viewName + "\" is already inherited from folder \"" + inheritedView.getContainer().getPath() + "\". "); // 11179: Allow editing the view if we're saving to session. @@ -2636,7 +2636,7 @@ else if (session != view.isSession()) try { view.delete(getUser(), getViewContext().getRequest()); - JSONObject ret = saveCustomView(container, queryDef, regionName, viewName, replaceExisting, share, inherit, explicitTargetContainer, session, saveFilter, hidden, jsonView, returnUrl, errors); + JSONObject ret = saveCustomView(container, queryDef, regionName, viewName, replaceExisting, share, inherit, inheritToTargetContainer, session, saveFilter, hidden, jsonView, returnUrl, errors); success = !errors.hasErrors() && ret != null; return success ? ret : null; } @@ -2782,9 +2782,9 @@ public ApiResponse execute(SimpleApiJsonForm form, BindException errors) boolean hidden = jsonView.optBoolean("hidden", false); // Users may save views to a location other than the current container String containerPath = jsonView.optString("containerPath", null); - boolean explicitTargetContainer = inherit && containerPath != null; + boolean inheritToTargetContainer = inherit && containerPath != null; Container container; - if (explicitTargetContainer) + if (inheritToTargetContainer) { // Only respect this request if it's a view that is inheritable in subfolders container = ContainerManager.getForPath(containerPath); @@ -2800,9 +2800,12 @@ public ApiResponse execute(SimpleApiJsonForm form, BindException errors) throw new NotFoundException("No such container: " + containerPath); } + if (inheritToTargetContainer && !container.hasPermission(getUser(), EditSharedViewPermission.class)) + throw new UnauthorizedException(); + JSONObject savedView = saveCustomView( container, queryDef, QueryView.DATAREGIONNAME_DEFAULT, viewName, replace, - shared, inherit, explicitTargetContainer, session, true, hidden, jsonView, null, errors); + shared, inherit, inheritToTargetContainer, session, true, hidden, jsonView, null, errors); if (savedView != null) { @@ -6232,9 +6235,9 @@ public ApiResponse execute(SaveSessionViewForm form, BindException errors) // Users may save views to a location other than the current container String containerPath = form.getContainerPath(); - boolean explicitTargetContainer = form.isInherit() && containerPath != null; + boolean inheritToTargetContainer = form.isInherit() && containerPath != null; Container container; - if (explicitTargetContainer) + if (inheritToTargetContainer) { // Only respect this request if it's a view that is inheritable in subfolders container = ContainerManager.getForPath(containerPath); @@ -6272,10 +6275,16 @@ public ApiResponse execute(SaveSessionViewForm form, BindException errors) existingView = form.getQueryDef().getCustomView(getUser(), null, form.getNewName()); } + // save a new private view if shared is false but existing view is shared + if (existingView != null && !form.isShared() && existingView.getOwner() == null) + { + existingView = null; + } + // GitHub Issue #899: getCustomView() also resolves views inherited from ancestor folders. Absent an explicit // target folder, shadow that view with a new local one instead of rewriting (and un-inheriting) the ancestor's. CustomView inheritedView = null; - if (existingView != null && !explicitTargetContainer && existingView.getContainer() != null + if (existingView != null && !inheritToTargetContainer && existingView.getContainer() != null && !container.equals(existingView.getContainer())) { inheritedView = existingView; @@ -6285,12 +6294,6 @@ public ApiResponse execute(SaveSessionViewForm form, BindException errors) if (inheritedView != null && !form.isReplace() && !StringUtils.isEmpty(form.getNewName())) throw new IllegalArgumentException("A saved view by the name \"" + form.getNewName() + "\" is already inherited from folder \"" + inheritedView.getContainer().getPath() + "\". "); - // save a new private view if shared is false but existing view is shared - if (existingView != null && !form.isShared() && existingView.getOwner() == null) - { - existingView = null; - } - if (existingView != null && !form.isReplace() && !StringUtils.isEmpty(form.getNewName())) throw new IllegalArgumentException("A saved view by the name \"" + form.getNewName() + "\" already exists. ");