diff --git a/assay/package-lock.json b/assay/package-lock.json index a64072f2048..69cc8b9c21b 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.59.0" }, "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.59.0", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.59.0.tgz", + "integrity": "sha512-wV5Wtqrz/dOzMWD6XpVuantMha8fetCYOXB7S/Jz6QMt2uE9Fg23lfxYArKWDylm8ufMgEzi9hqDmYdeYM6xYg==", "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..0d49e947c9b 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.59.0" }, "devDependencies": { "@labkey/build": "10.1.2", diff --git a/core/package-lock.json b/core/package-lock.json index aabeeddc3d2..02c67966ae5 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.59.0", "@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.59.0", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.59.0.tgz", + "integrity": "sha512-wV5Wtqrz/dOzMWD6XpVuantMha8fetCYOXB7S/Jz6QMt2uE9Fg23lfxYArKWDylm8ufMgEzi9hqDmYdeYM6xYg==", "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..6020e70b9c0 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.59.0", "@labkey/themes": "1.9.5" }, "devDependencies": { diff --git a/experiment/package-lock.json b/experiment/package-lock.json index 35a8dd69da5..c11eb5a0321 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.59.0" }, "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.59.0", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.59.0.tgz", + "integrity": "sha512-wV5Wtqrz/dOzMWD6XpVuantMha8fetCYOXB7S/Jz6QMt2uE9Fg23lfxYArKWDylm8ufMgEzi9hqDmYdeYM6xYg==", "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..45c776c7d4c 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.59.0" }, "devDependencies": { "@labkey/build": "10.1.2", diff --git a/pipeline/package-lock.json b/pipeline/package-lock.json index 062c30b8df0..2e1ad5a03d9 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.59.0" }, "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.59.0", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.59.0.tgz", + "integrity": "sha512-wV5Wtqrz/dOzMWD6XpVuantMha8fetCYOXB7S/Jz6QMt2uE9Fg23lfxYArKWDylm8ufMgEzi9hqDmYdeYM6xYg==", "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..db925d09b3f 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.59.0" }, "devDependencies": { "@labkey/build": "10.1.2", diff --git a/query/src/org/labkey/query/controllers/QueryController.java b/query/src/org/labkey/query/controllers/QueryController.java index 8603332e28b..f118d3862bb 100644 --- a/query/src/org/labkey/query/controllers/QueryController.java +++ b/query/src/org/labkey/query/controllers/QueryController.java @@ -2531,10 +2531,42 @@ public void addNavTrail(NavTree root) } } + /** + * GitHub Issue #899: custom view lookups also resolve views inherited from ancestor folders. Absent an explicit target + * folder, such a view must be shadowed by a new local one instead of rewritten (and un-inherited), so a name collision + * with an ancestor's view reports differently from one with a local view. + * + * @param localView the resolved view, null once it turns out to belong to an ancestor + * @param message a name-collision error, or null if the save may proceed + */ + private record ResolvedViewName(CustomView localView, String message) {} + + private static ResolvedViewName resolveViewName(CustomView existingView, String name, Container container, + boolean inheritToTargetContainer, boolean replaceExisting) + { + CustomView inheritedView = null; + if (existingView != null && !inheritToTargetContainer && existingView.getContainer() != null + && !container.equals(existingView.getContainer())) + { + inheritedView = existingView; + existingView = null; + } + + String message = null; + if (!replaceExisting && !StringUtils.isEmpty(name)) + { + if (inheritedView != null) + message = "A saved view by the name \"" + name + "\" is already inherited from folder \"" + inheritedView.getContainer().getPath() + "\". "; + else if (existingView != null) + message = "A saved view by the name \"" + name + "\" already exists. "; + } + return new ResolvedViewName(existingView, message); + } + // 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 inheritToTargetContainer, boolean session, boolean saveFilter, boolean hidden, JSONObject jsonView, ActionURL returnUrl, @@ -2558,8 +2590,10 @@ protected JSONObject saveCustomView(Container container, QueryDefinition queryDe else view = queryDef.getCustomView(owner, getViewContext().getRequest(), name); - if (view != null && !replaceExisting && !StringUtils.isEmpty(name)) - errors.reject(ERROR_MSG, "A saved view by the name \"" + viewName + "\" already exists. "); + ResolvedViewName resolved = resolveViewName(view, name, container, inheritToTargetContainer, replaceExisting); + view = resolved.localView(); + if (resolved.message() != null) + errors.reject(ERROR_MSG, resolved.message()); // 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. @@ -2625,7 +2659,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, inheritToTargetContainer, session, saveFilter, hidden, jsonView, returnUrl, errors); success = !errors.hasErrors() && ret != null; return success ? ret : null; } @@ -2770,9 +2804,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 inheritToTargetContainer = inherit && containerPath != null; Container container; - if (inherit) + if (inheritToTargetContainer) { // Only respect this request if it's a view that is inheritable in subfolders container = ContainerManager.getForPath(containerPath); @@ -2788,9 +2823,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, session, true, hidden, jsonView, null, errors); + shared, inherit, inheritToTargetContainer, session, true, hidden, jsonView, null, errors); if (savedView != null) { @@ -6220,8 +6258,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 inheritToTargetContainer = form.isInherit() && containerPath != null; Container container; - if (form.isInherit() && containerPath != null) + if (inheritToTargetContainer) { // Only respect this request if it's a view that is inheritable in subfolders container = ContainerManager.getForPath(containerPath); @@ -6265,8 +6304,10 @@ public ApiResponse execute(SaveSessionViewForm form, BindException errors) existingView = null; } - if (existingView != null && !form.isReplace() && !StringUtils.isEmpty(form.getNewName())) - throw new IllegalArgumentException("A saved view by the name \"" + form.getNewName() + "\" already exists. "); + ResolvedViewName resolved = resolveViewName(existingView, form.getNewName(), container, inheritToTargetContainer, form.isReplace()); + existingView = resolved.localView(); + if (resolved.message() != null) + throw new IllegalArgumentException(resolved.message()); if (existingView == null || (existingView instanceof ModuleCustomView && existingView.isEditable())) { @@ -6278,8 +6319,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()); }