From 333ce3e495dc4cde0e1e4a3fb8163176c810ec48 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 24 Aug 2026 10:22:42 -0500 Subject: [PATCH 1/8] GitHub Issue #899: Save grid view fix for tracking the shadow view properties when saving session view --- .../src/public/QueryModel/SaveViewModal.tsx | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/components/src/public/QueryModel/SaveViewModal.tsx b/packages/components/src/public/QueryModel/SaveViewModal.tsx index de112ec186..7e47a31864 100644 --- a/packages/components/src/public/QueryModel/SaveViewModal.tsx +++ b/packages/components/src/public/QueryModel/SaveViewModal.tsx @@ -134,11 +134,17 @@ export const SaveViewModal: FC = memo(props => { const [isDefaultView, setIsDefaultView] = useState( () => user.hasAdminPermission() && currentView?.isDefault ); - const [canInherit, setCanInherit] = useState(currentView?.inherit); - const [isShared, setIsShared] = useState(currentView?.shared); const [errorMessage, setErrorMessage] = useState(); const [isSubmitting, setIsSubmitting] = useState(); const canEditShared = userCanEditSharedViews(user); + const showInheritOption = + isProductFoldersEnabled(moduleContext) && isAppHomeFolder(container, moduleContext) && canEditShared; + // GitHub Issue #899: a session view is never shared or inheritable, so read both flags off the view it shadows. + const [isShared, setIsShared] = useState(() => !!(currentView?.shadowed?.shared ?? currentView?.shared)); + // GitHub Issue #899: Outside the home folder inherit isn't offered, and sending it would target the folder the view is inherited from. + const [canInherit, setCanInherit] = useState( + () => showInheritOption && !!(currentView?.shadowed?.inherit ?? currentView?.inherit) + ); const saveView = useCallback(async () => { if (!viewName && !isDefaultView) return; @@ -243,23 +249,21 @@ export const SaveViewModal: FC = memo(props => { )} - {isProductFoldersEnabled(moduleContext) && - isAppHomeFolder(container, moduleContext) && - canEditShared && ( -
- - - Make this grid view available in all Folders - -
- )} + {showInheritOption && ( +
+ + + Make this grid view available in all Folders + +
+ )}
Learn more about custom grid views in LabKey.
From 4e75f71327878c286aca15e5f0fecf99e85bbd33 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 24 Aug 2026 10:24:02 -0500 Subject: [PATCH 2/8] GitHub Issue #899: Save grid view fix for tracking the shadow view properties when saving session view --- packages/components/src/internal/ViewInfo.ts | 4 + .../public/QueryModel/SaveViewModal.test.tsx | 128 ++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/packages/components/src/internal/ViewInfo.ts b/packages/components/src/internal/ViewInfo.ts index 5d5c53b64a..0a1c7c90c1 100644 --- a/packages/components/src/internal/ViewInfo.ts +++ b/packages/components/src/internal/ViewInfo.ts @@ -55,6 +55,7 @@ export interface ViewInfoJson { savable?: boolean; saved?: boolean; session?: boolean; + shadowed?: ViewInfoJson; shared?: boolean; sort?: QuerySortJson[]; } @@ -71,6 +72,7 @@ const VIEW_INFO_DEFAULTS = { savable: false, saved: false, session: false, + shadowed: undefined, shared: false, sorts: [], }; @@ -92,6 +94,7 @@ export class ViewInfo { declare savable: boolean; declare saved: boolean; declare session: boolean; + declare shadowed: ViewInfoJson; // The saved view a session view is overlaying; only present when session is true declare shared: boolean; declare sorts: QuerySort[]; @@ -132,6 +135,7 @@ export class ViewInfo { const json = rest as unknown as ViewInfoJson; delete json.fields; // Issue 53324: not needed for serialization and takes up space + delete json.shadowed; // read-only server-supplied detail, and takes up space json.columns = [...columns]; json.default = isDefault; diff --git a/packages/components/src/public/QueryModel/SaveViewModal.test.tsx b/packages/components/src/public/QueryModel/SaveViewModal.test.tsx index da5818f1fe..e2b5c279df 100644 --- a/packages/components/src/public/QueryModel/SaveViewModal.test.tsx +++ b/packages/components/src/public/QueryModel/SaveViewModal.test.tsx @@ -45,6 +45,36 @@ describe('SaveViewModal', () => { inherit: true, }); + const SESSION_VIEW_SHADOWING_INHERITED = ViewInfo.fromJson({ + default: true, + inherit: false, + session: true, + shadowed: { default: true, inherit: true }, + }); + + const SESSION_VIEW_SHADOWING_LOCAL = ViewInfo.fromJson({ + default: true, + inherit: false, + session: true, + shadowed: { default: true, inherit: false }, + }); + + const SESSION_VIEW_SHADOWING_SHARED = ViewInfo.fromJson({ + label: 'View 1', + name: 'View1', + session: true, + shared: false, + shadowed: { name: 'View1', shared: true }, + }); + + const SESSION_VIEW_SHADOWING_PRIVATE = ViewInfo.fromJson({ + label: 'View 1', + name: 'View1', + session: true, + shared: false, + shadowed: { name: 'View1', shared: false }, + }); + const moduleContext = { query: { isProductFoldersEnabled: true, @@ -163,6 +193,104 @@ describe('SaveViewModal', () => { expect(document.querySelectorAll('input[name="setInherit"]')).toHaveLength(0); expect(document.querySelectorAll('input[name="setShared"]')).toHaveLength(0); }); + + test('session view uses the shadowed view inherit flag', () => { + renderWithAppContext(, { + serverContext: { + user: TEST_USER_APP_ADMIN, + container: { + path: '/home', + type: 'project', + }, + moduleContext, + }, + }); + + expect(document.querySelector('input[name="setInherit"]').hasAttribute('checked')).toBe(true); + }); + + test('session view shadowing a view that is not inherited', () => { + renderWithAppContext(, { + serverContext: { + user: TEST_USER_APP_ADMIN, + container: { + path: '/home', + type: 'project', + }, + moduleContext, + }, + }); + + expect(document.querySelector('input[name="setInherit"]').hasAttribute('checked')).toBe(false); + }); + + // GitHub Issue #899 + test('subfolder save does not inherit', async () => { + const onConfirmSave = jest.fn(); + renderWithAppContext( + , + { + serverContext: { + user: TEST_USER_APP_ADMIN, + container: { + path: '/home/folderA', + type: 'folder', + }, + moduleContext, + }, + } + ); + + expect(document.querySelectorAll('input[name="setInherit"]')).toHaveLength(0); + + await userEvent.click(document.querySelector('.btn-success')); + + // canInherit must be false: the inherited view lives in the home folder, so saving it would target that folder + expect(onConfirmSave).toHaveBeenCalledWith('', false, false, true); + }); + + test('session view uses the shadowed view shared flag', async () => { + const onConfirmSave = jest.fn(); + renderWithAppContext( + , + { + serverContext: { + user: TEST_USER_PROJECT_ADMIN, + container: { + path: '/home', + type: 'project', + }, + moduleContext, + }, + } + ); + + expect(document.querySelector('input[name="setShared"]').hasAttribute('checked')).toBe(true); + + await userEvent.click(document.querySelector('.btn-success')); + + // re-saving a shared view must not silently demote it to a private view shadowing the shared one + expect(onConfirmSave).toHaveBeenCalledWith('View1', false, true, true); + }); + + test('session view shadowing a private view', () => { + renderWithAppContext(, { + serverContext: { + user: TEST_USER_PROJECT_ADMIN, + container: { + path: '/home', + type: 'project', + }, + moduleContext, + }, + }); + + expect(document.querySelector('input[name="setShared"]').hasAttribute('checked')).toBe(false); + }); }); describe('ViewNameInput', () => { From c842ba0c0b9d86c91c63b508fed447ef8f74e321 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 24 Aug 2026 10:25:11 -0500 Subject: [PATCH 3/8] 7.58.6-fb-saveView899.0 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 3bf56ca420..a1ebc4f165 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.58.5", + "version": "7.58.6-fb-saveView899.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.58.5", + "version": "7.58.6-fb-saveView899.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index edf2eb6e44..07f241f98f 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.58.5", + "version": "7.58.6-fb-saveView899.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 7787dfdbeb6fbfb57543c0364edf40de1be9568f Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 24 Aug 2026 15:07:09 -0500 Subject: [PATCH 4/8] Similar fix for ManageViewsModal.tsx to use shared view props in session case --- .../QueryModel/ManageViewsModal.test.tsx | 80 +++++++++++++++++++ .../public/QueryModel/ManageViewsModal.tsx | 29 ++----- 2 files changed, 88 insertions(+), 21 deletions(-) diff --git a/packages/components/src/public/QueryModel/ManageViewsModal.test.tsx b/packages/components/src/public/QueryModel/ManageViewsModal.test.tsx index 8290d112e5..2ea14a8275 100644 --- a/packages/components/src/public/QueryModel/ManageViewsModal.test.tsx +++ b/packages/components/src/public/QueryModel/ManageViewsModal.test.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { waitFor } from '@testing-library/dom'; +import { userEvent } from '@testing-library/user-event'; import { ViewInfo } from '../../internal/ViewInfo'; @@ -65,6 +66,18 @@ const SHARED_VIEW = ViewInfo.fromJson({ shared: true, }); +// as the server reports it from a subfolder: inheritable and owned by the home folder +const INHERITED_VIEW = ViewInfo.fromJson({ + containerPath: '/project', + default: false, + inherit: true, + label: 'View 4', + name: 'View4', + shared: true, +}); + +const FOLDERS_ON = { query: { isProductFoldersEnabled: true } }; + describe('ViewLabel', () => { test('default view', () => { const { container } = render(); @@ -245,6 +258,73 @@ describe('ManageViewsModal', () => { expect(rows[0].querySelector('.gray-text').textContent).toBe('Revert'); }); + // GitHub Issue #899 + test('make default from a subfolder does not inherit', async () => { + const api = getQueryAPI([INHERITED_VIEW]); + renderWithAppContext( + , + { + appContext: { api }, + serverContext: { + user: TEST_USER_PROJECT_ADMIN, + container: { path: '/project/a', type: 'folder' }, + moduleContext: FOLDERS_ON, + }, + } + ); + await waitFor(() => { + expect(document.querySelector('#setDefault-0')).not.toBeNull(); + }); + + await userEvent.click(document.querySelector('#setDefault-0')); + + // inherit must be false: the view lives in the home folder, so inheriting would promote it there instead + await waitFor(() => { + expect(api.query.saveGridView).toHaveBeenCalledWith( + null, + undefined, + expect.anything(), + true, + false, + false, + true + ); + }); + }); + + // GitHub Issue #899 + test('make default from the home folder keeps inherit', async () => { + const api = getQueryAPI([INHERITED_VIEW]); + renderWithAppContext( + , + { + appContext: { api }, + serverContext: { + user: TEST_USER_PROJECT_ADMIN, + container: { path: '/project', type: 'project' }, + moduleContext: FOLDERS_ON, + }, + } + ); + await waitFor(() => { + expect(document.querySelector('#setDefault-0')).not.toBeNull(); + }); + + await userEvent.click(document.querySelector('#setDefault-0')); + + await waitFor(() => { + expect(api.query.saveGridView).toHaveBeenCalledWith( + null, + undefined, + expect.anything(), + true, + false, + true, + true + ); + }); + }); + test('multiple saved views: no admin permission', async () => { renderWithAppContext(, { appContext: { api: getQueryAPI([MY_DEFAULT_VIEW, VIEW_1, SESSION_VIEW, SHARED_VIEW]) }, diff --git a/packages/components/src/public/QueryModel/ManageViewsModal.tsx b/packages/components/src/public/QueryModel/ManageViewsModal.tsx index 8ed9b0483a..0a3458ee3e 100644 --- a/packages/components/src/public/QueryModel/ManageViewsModal.tsx +++ b/packages/components/src/public/QueryModel/ManageViewsModal.tsx @@ -15,7 +15,7 @@ import { resolveErrorMessage } from '../../internal/util/messaging'; import { RequiresPermission } from '../../internal/components/base/Permissions'; -import { userCanEditSharedViews } from '../../internal/app/utils'; +import { canInheritGridView, userCanEditSharedViews } from '../../internal/app/utils'; import { Modal } from '../../internal/Modal'; import { OverlayTrigger } from '../../internal/OverlayTrigger'; @@ -57,8 +57,9 @@ export const ManageViewsModal: FC = memo(props => { const [deleting, setDeleting] = useState(); const { api } = useAppContext(); - const { user } = useServerContext(); + const { container, moduleContext, user } = useServerContext(); const userCanEditShared = userCanEditSharedViews(user); + const canInheritView = canInheritGridView(user, container, moduleContext); useEffect(() => { (async () => { @@ -121,31 +122,17 @@ export const ManageViewsModal: FC = memo(props => { const view = getActionView(event); handleAction(async () => { const finalViewInfo = view.mutate({ name: '' }); + // GitHub Issue #899: from a subfolder this must shadow the inherited view, not promote the parent's. + const inherit = view.inherit && canInheritView; if (view.session) { - await api.query.saveSessionView( - schemaQuery, - containerPath, - view.name, - '', - view.inherit, - true, - true - ); + await api.query.saveSessionView(schemaQuery, containerPath, view.name, '', inherit, true, true); } else { - await api.query.saveGridView( - schemaQuery, - containerPath, - finalViewInfo, - true, - false, - view.inherit, - true - ); + await api.query.saveGridView(schemaQuery, containerPath, finalViewInfo, true, false, inherit, true); } if (currentView.name === view.name) setReselectViewName(''); }); }, - [api, getActionView, handleAction, currentView, schemaQuery, containerPath] + [api, getActionView, handleAction, currentView, schemaQuery, containerPath, canInheritView] ); const deleteSavedView = useCallback(() => { From 92d8e7853a35cb26d7615885871726c3fe092a66 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 24 Aug 2026 15:08:37 -0500 Subject: [PATCH 5/8] canInheritGridView shared helper for save and manage views modals --- .../components/src/internal/app/utils.test.ts | 25 ++++++ packages/components/src/internal/app/utils.ts | 6 ++ .../public/QueryModel/SaveViewModal.test.tsx | 80 +++++++++++++++++++ .../src/public/QueryModel/SaveViewModal.tsx | 13 +-- 4 files changed, 118 insertions(+), 6 deletions(-) diff --git a/packages/components/src/internal/app/utils.test.ts b/packages/components/src/internal/app/utils.test.ts index 6095f5baa7..f6797a7fd4 100644 --- a/packages/components/src/internal/app/utils.test.ts +++ b/packages/components/src/internal/app/utils.test.ts @@ -40,6 +40,7 @@ import { import { addAssaysSectionConfig, addSourcesSectionConfig, + canInheritGridView, freezerManagerIsCurrentApp, getCurrentAppProperties, getMenuSectionConfigs, @@ -998,6 +999,30 @@ describe('utils', () => { ).toBeFalsy(); }); + // GitHub Issue #899 + test('canInheritGridView', () => { + const HOME = new Container({ type: 'project', path: 'project' }); + const SUBFOLDER = new Container({ type: 'folder', path: 'project/a' }); + const FOLDERS_ON = { query: { isProductFoldersEnabled: true } }; + const FOLDERS_OFF = { query: { isProductFoldersEnabled: false } }; + + expect(canInheritGridView(TEST_USER_EDITOR, HOME, FOLDERS_ON)).toBeTruthy(); + expect(canInheritGridView(TEST_USER_APP_ADMIN, HOME, FOLDERS_ON)).toBeTruthy(); + expect(canInheritGridView(TEST_USER_FOLDER_ADMIN, HOME, FOLDERS_ON)).toBeTruthy(); + + // an inherited view lives in the home folder, so a subfolder save must shadow it rather than target it + expect(canInheritGridView(TEST_USER_APP_ADMIN, SUBFOLDER, FOLDERS_ON)).toBeFalsy(); + + // without product folders every container is the app home folder + expect(canInheritGridView(TEST_USER_APP_ADMIN, SUBFOLDER, FOLDERS_OFF)).toBeTruthy(); + expect(canInheritGridView(TEST_USER_APP_ADMIN, HOME, FOLDERS_OFF)).toBeTruthy(); + + // the save actions reject inherit outright without EditSharedView + expect(canInheritGridView(TEST_USER_READER, HOME, FOLDERS_ON)).toBeFalsy(); + expect(canInheritGridView(TEST_USER_AUTHOR, HOME, FOLDERS_ON)).toBeFalsy(); + expect(canInheritGridView(TEST_USER_GUEST, HOME, FOLDERS_ON)).toBeFalsy(); + expect(canInheritGridView(TEST_USER_READER, SUBFOLDER, FOLDERS_OFF)).toBeFalsy(); + }); test('getPrimaryAppProperties', () => { __setController('project'); diff --git a/packages/components/src/internal/app/utils.ts b/packages/components/src/internal/app/utils.ts index b9df29c2ad..2c3c07b1b5 100644 --- a/packages/components/src/internal/app/utils.ts +++ b/packages/components/src/internal/app/utils.ts @@ -183,6 +183,12 @@ export function isAppHomeFolder(container?: Partial, moduleContext?: return isTopFolder || (isSubFolder && !isProductFoldersEnabled(moduleContext)); } +// GitHub Issue #899: outside the app home folder an inherited view lives in the parent containerPath, so saving it +// with inherit would target that parent instead of shadowing it locally. +export function canInheritGridView(user: User, container?: Partial, moduleContext?: ModuleContext): boolean { + return userCanEditSharedViews(user) && isAppHomeFolder(container, moduleContext); +} + export function getAppHomeFolderPath(container?: Partial, moduleContext?: ModuleContext): string { const currentContainer: Partial = container ?? getServerContext().container; return isAppHomeFolder(currentContainer, moduleContext) ? currentContainer.path : currentContainer.parentPath; diff --git a/packages/components/src/public/QueryModel/SaveViewModal.test.tsx b/packages/components/src/public/QueryModel/SaveViewModal.test.tsx index e2b5c279df..f12e38f5cd 100644 --- a/packages/components/src/public/QueryModel/SaveViewModal.test.tsx +++ b/packages/components/src/public/QueryModel/SaveViewModal.test.tsx @@ -291,6 +291,86 @@ describe('SaveViewModal', () => { expect(document.querySelector('input[name="setShared"]').hasAttribute('checked')).toBe(false); }); + + // GitHub Issue #899: the core scenario — a subfolder session view over a view inherited from the home folder + test('subfolder session view shadowing an inherited view does not inherit', async () => { + const onConfirmSave = jest.fn(); + renderWithAppContext( + , + { + serverContext: { + user: TEST_USER_APP_ADMIN, + container: { + path: '/home/folderA', + type: 'folder', + }, + moduleContext, + }, + } + ); + + expect(document.querySelectorAll('input[name="setInherit"]')).toHaveLength(0); + + await userEvent.click(document.querySelector('.btn-success')); + + // shadowed.inherit is true, but sending it would rewrite the home folder's view instead of shadowing it here + expect(onConfirmSave).toHaveBeenCalledWith('', false, false, true); + }); + + // GitHub Issue #899 + test('session view shadowing a shared view without edit-shared permission', async () => { + const onConfirmSave = jest.fn(); + renderWithAppContext( + , + { + serverContext: { + user: TEST_USER_READER, + container: { + path: '/home', + type: 'project', + }, + moduleContext, + }, + } + ); + + expect(document.querySelectorAll('input[name="setShared"]')).toHaveLength(0); + + await userEvent.click(document.querySelector('.btn-success')); + + // the shadowed view's shared flag is unusable here: the save action rejects shared/inherit outright + expect(onConfirmSave).toHaveBeenCalledWith('View1', false, true, false); + }); + + // GitHub Issue #899 + test('inherit flag preserved when product folders are disabled', async () => { + const onConfirmSave = jest.fn(); + renderWithAppContext(, { + serverContext: { + user: TEST_USER_PROJECT_ADMIN, + container: { + path: '/home', + type: 'project', + }, + moduleContext: { query: { isProductFoldersEnabled: false } }, + }, + }); + + // the checkbox is product-folders-only, but the view's own folder is the save target so inherit still applies + expect(document.querySelectorAll('input[name="setInherit"]')).toHaveLength(0); + + await userEvent.click(document.querySelector('.btn-success')); + + expect(onConfirmSave).toHaveBeenCalledWith('View2', true, true, false); + }); }); describe('ViewNameInput', () => { diff --git a/packages/components/src/public/QueryModel/SaveViewModal.tsx b/packages/components/src/public/QueryModel/SaveViewModal.tsx index 7e47a31864..86cfcfe938 100644 --- a/packages/components/src/public/QueryModel/SaveViewModal.tsx +++ b/packages/components/src/public/QueryModel/SaveViewModal.tsx @@ -13,7 +13,7 @@ import { Alert } from '../../internal/components/base/Alert'; import { resolveErrorMessage } from '../../internal/util/messaging'; import { CUSTOM_VIEW, HelpLink } from '../../internal/util/helpLinks'; import { RequiresPermission } from '../../internal/components/base/Permissions'; -import { isAppHomeFolder, isProductFoldersEnabled, userCanEditSharedViews } from '../../internal/app/utils'; +import { canInheritGridView, isProductFoldersEnabled, userCanEditSharedViews } from '../../internal/app/utils'; import { useServerContext } from '../../internal/components/base/ServerContext'; import { ViewInfo } from '../../internal/ViewInfo'; @@ -137,13 +137,14 @@ export const SaveViewModal: FC = memo(props => { const [errorMessage, setErrorMessage] = useState(); const [isSubmitting, setIsSubmitting] = useState(); const canEditShared = userCanEditSharedViews(user); - const showInheritOption = - isProductFoldersEnabled(moduleContext) && isAppHomeFolder(container, moduleContext) && canEditShared; + const canInheritView = canInheritGridView(user, container, moduleContext); + const showInheritOption = isProductFoldersEnabled(moduleContext) && canInheritView; // GitHub Issue #899: a session view is never shared or inheritable, so read both flags off the view it shadows. - const [isShared, setIsShared] = useState(() => !!(currentView?.shadowed?.shared ?? currentView?.shared)); - // GitHub Issue #899: Outside the home folder inherit isn't offered, and sending it would target the folder the view is inherited from. + const [isShared, setIsShared] = useState( + () => canEditShared && !!(currentView?.shadowed?.shared ?? currentView?.shared) + ); const [canInherit, setCanInherit] = useState( - () => showInheritOption && !!(currentView?.shadowed?.inherit ?? currentView?.inherit) + () => canInheritView && !!(currentView?.shadowed?.inherit ?? currentView?.inherit) ); const saveView = useCallback(async () => { From e43379a38e41f1141e2ce4db0c267bddcc2f9e9e Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 24 Aug 2026 15:09:08 -0500 Subject: [PATCH 6/8] ViewInfo to drop containerPath on serialize --- packages/components/src/internal/ViewInfo.test.ts | 5 +++++ packages/components/src/internal/ViewInfo.ts | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/components/src/internal/ViewInfo.test.ts b/packages/components/src/internal/ViewInfo.test.ts index af3a8074b4..5cebad6d78 100644 --- a/packages/components/src/internal/ViewInfo.test.ts +++ b/packages/components/src/internal/ViewInfo.test.ts @@ -46,6 +46,11 @@ describe('ViewInfo', () => { }); expect(ViewInfo.serialize(view).fields).toBeUndefined(); expect(ViewInfo.serialize(view).columns).toStrictEqual([{ name: 'col1', key: 'col1', fieldKey: 'col1' }]); + + // GitHub Issue #899: saveQueryViews reads containerPath as an explicit save target + view = ViewInfo.fromJson({ name: 'test', containerPath: '/home', shadowed: { name: 'test' } }); + expect(ViewInfo.serialize(view).containerPath).toBeUndefined(); + expect(ViewInfo.serialize(view).shadowed).toBeUndefined(); }); test('isVisible', () => { diff --git a/packages/components/src/internal/ViewInfo.ts b/packages/components/src/internal/ViewInfo.ts index 0a1c7c90c1..bd901e8b2c 100644 --- a/packages/components/src/internal/ViewInfo.ts +++ b/packages/components/src/internal/ViewInfo.ts @@ -42,6 +42,7 @@ export interface ViewInfoJson { // aggregates: any[]; // analyticsProviders: any[]; columns?: ViewInfoColumn[]; + containerPath?: string; default?: boolean; // deletable: boolean; // editable: boolean; @@ -94,7 +95,7 @@ export class ViewInfo { declare savable: boolean; declare saved: boolean; declare session: boolean; - declare shadowed: ViewInfoJson; // The saved view a session view is overlaying; only present when session is true + declare shadowed?: ViewInfoJson; // The saved view a session view is overlaying; only present when session is true declare shared: boolean; declare sorts: QuerySort[]; @@ -136,6 +137,7 @@ export class ViewInfo { delete json.fields; // Issue 53324: not needed for serialization and takes up space delete json.shadowed; // read-only server-supplied detail, and takes up space + delete json.containerPath; // GitHub Issue #899: saveQueryViews treats containerPath as an explicit save target json.columns = [...columns]; json.default = isDefault; From 01e3123db56bf7fb0f17671e654e0db938fdf758 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 24 Aug 2026 15:09:19 -0500 Subject: [PATCH 7/8] 7.58.6-fb-saveView899.1 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index a1ebc4f165..614db55281 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.58.6-fb-saveView899.0", + "version": "7.58.6-fb-saveView899.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.58.6-fb-saveView899.0", + "version": "7.58.6-fb-saveView899.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 07f241f98f..15d4f6e93f 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.58.6-fb-saveView899.0", + "version": "7.58.6-fb-saveView899.1", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 6f310c59b1b1c806caffe6f2ece9dcffdfbadad0 Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 25 Aug 2026 09:16:41 -0500 Subject: [PATCH 8/8] Claude CR - add canInheritGridView check to grid title save button to match save modal --- .../src/public/QueryModel/GridPanel.test.tsx | 44 +++++++++++++++++++ .../src/public/QueryModel/GridPanel.tsx | 22 +++++++--- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/packages/components/src/public/QueryModel/GridPanel.test.tsx b/packages/components/src/public/QueryModel/GridPanel.test.tsx index 04565e539f..9bd05cd29e 100644 --- a/packages/components/src/public/QueryModel/GridPanel.test.tsx +++ b/packages/components/src/public/QueryModel/GridPanel.test.tsx @@ -731,6 +731,50 @@ describe('GridTitle', () => { validate(container, 'Default View', true, true, false, true); }); + // GitHub Issue #899 + const renderSaveCurrentView = (onSaveView: jest.Mock, path: string, type: string) => { + const viewSchemaQuery = new SchemaQuery('exp.data', 'mixtures', 'noExtraColumn'); + const sessionQueryInfo = QUERY_INFO.mutate({ + views: QUERY_INFO.views.merge({ + noextracolumn: QUERY_INFO.views.get('noextracolumn').mutate({ inherit: true, session: true }), + }), + }); + return renderWithAppContext( + , + { + serverContext: { + user: TEST_USER_PROJECT_ADMIN, + container: { path, type }, + moduleContext: { query: { isProductFoldersEnabled: true } }, + }, + } + ); + }; + + test('save current view from a subfolder does not inherit', async () => { + const onSaveView = jest.fn(); + const { container } = renderSaveCurrentView(onSaveView, '/project/a', 'folder'); + + await userEvent.click(container.querySelector('.split-button-dropdown__button')); + + // the inherited view lives in the home folder, so a subfolder save must shadow it rather than target it + expect(onSaveView).toHaveBeenCalledWith(true, false); + }); + + test('save current view from the home folder keeps inherit', async () => { + const onSaveView = jest.fn(); + const { container } = renderSaveCurrentView(onSaveView, '/project', 'project'); + + await userEvent.click(container.querySelector('.split-button-dropdown__button')); + + expect(onSaveView).toHaveBeenCalledWith(true, true); + }); + test('hidden view, not edited, no title', () => { const viewSchemaQuery = new SchemaQuery('exp.data', 'mixtures', 'noExtraColumn'); const sessionQueryInfo = QUERY_INFO.mutate({ diff --git a/packages/components/src/public/QueryModel/GridPanel.tsx b/packages/components/src/public/QueryModel/GridPanel.tsx index 6baad00d17..a8dce90968 100644 --- a/packages/components/src/public/QueryModel/GridPanel.tsx +++ b/packages/components/src/public/QueryModel/GridPanel.tsx @@ -47,7 +47,7 @@ import { Grid } from '../../internal/components/base/Grid'; import { Alert } from '../../internal/components/base/Alert'; -import { userCanEditSharedViews } from '../../internal/app/utils'; +import { canInheritGridView, userCanEditSharedViews } from '../../internal/app/utils'; import { User } from '../../internal/components/base/models/User'; @@ -286,7 +286,7 @@ interface GridTitleProps { model: QueryModel; onRevertView?: () => void; onSaveNewView?: () => void; - onSaveView?: (canSaveShared) => void; + onSaveView?: (canSaveShared: boolean, canInherit: boolean) => void; title?: string; view?: ViewInfo; } @@ -306,7 +306,7 @@ export const GridTitle: FC = memo(props => { } = props; const { viewName } = model; const [errorMsg, setErrorMsg] = useState(); - const { user } = useServerContext(); + const { container, moduleContext, user } = useServerContext(); const currentView = view ?? model.currentView; let displayTitle = title; @@ -336,8 +336,11 @@ export const GridTitle: FC = memo(props => { }, [model, onRevertView, actions, allowSelections]); const _onSaveCurrentView = useCallback((): void => { - onSaveView(userCanEditSharedViews(user as User)); - }, [onSaveView, user]); + onSaveView( + userCanEditSharedViews(user as User), + canInheritGridView(user as User, container, moduleContext) + ); + }, [container, moduleContext, onSaveView, user]); if (!displayTitle && (!allowViewCustomization || (!isEdited && !isUpdated))) { return null; @@ -801,7 +804,7 @@ export class GridPanel extends PureComponent, State> { }); }; - onSaveCurrentView = async (canSaveShared: boolean): Promise => { + onSaveCurrentView = async (canSaveShared: boolean, canInherit: boolean): Promise => { const { model } = this.props; const { queryInfo, viewName } = model; const view = queryInfo?.getView(viewName, true); @@ -809,7 +812,12 @@ export class GridPanel extends PureComponent, State> { let currentView = view; try { if (view.session) currentView = await getGridView(queryInfo.schemaQuery, viewName, true); - await this.onSaveView(viewName, currentView?.inherit, true, currentView.shared && canSaveShared); + await this.onSaveView( + viewName, + currentView?.inherit && canInherit, + true, + currentView.shared && canSaveShared + ); } catch (errorMsg) { this.setState({ errorMsg }); }