Skip to content
8 changes: 4 additions & 4 deletions assay/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion assay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
8 changes: 4 additions & 4 deletions experiment/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion experiment/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions pipeline/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pipeline/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
64 changes: 52 additions & 12 deletions query/src/org/labkey/query/controllers/QueryController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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)
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()))
{
Expand All @@ -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());
}
Expand Down