From 4d90362ae24f092bebc31a1e9e417fae810af372 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 21:14:33 +0000 Subject: [PATCH 1/2] fix(app-shell): creating an item or opening one from Pending changes asks before it drops a held Studio edit A Data, Automations or Interfaces pillar now asks the shared departure question before its New dialog opens while its autosave holds an edit, and the Data pillar asks before a Pending changes request opens another object in the same pillar. Declined, the held edit stays. Claude-Session: https://claude.ai/code/session_01JQwxbZTZSxa584VWULEuMZ Co-Authored-By: Claude --- .../studio-design/StudioDesignSurface.tsx | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx index eb0b0dda85f..bd7f5c9aed8 100644 --- a/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx +++ b/ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx @@ -4161,12 +4161,19 @@ export function InterfacesPillar({ const [createError, setCreateError] = React.useState(null); const [reportBinding, setReportBinding] = React.useState(NO_REPORT_BINDING); const [pageKind, setPageKind] = React.useState(DEFAULT_PAGE_SOURCE_KIND); - const openCreate = React.useCallback((kind: InterfaceCreateKind) => { - setCreateError(null); - setReportBinding(NO_REPORT_BINDING); - setPageKind(DEFAULT_PAGE_SOURCE_KIND); - setCreateKind(kind); - }, []); + const openCreate = React.useCallback( + (kind: InterfaceCreateKind) => { + // objectstack#26845 — the created item opens at once, and its load + // replaces the buffer a held widget lives in: asked before the dialog + // opens, so Cancel costs nothing and the create itself asks nothing. + if (!confirmDropUnsavedWork(leafHeld !== null, locale)) return; + setCreateError(null); + setReportBinding(NO_REPORT_BINDING); + setPageKind(DEFAULT_PAGE_SOURCE_KIND); + setCreateKind(kind); + }, + [leafHeld, locale], + ); // objectui#12455 — the "+" kind picker's other kinds: the entry is added // where "Add nav item" adds one (the end of the top level, an id minted the // way `AppNavCanvas` mints it), born with no label (objectui#11196) and, for @@ -5500,15 +5507,10 @@ export function DataPillar({ // they had since selected, which is the regression the mount-time ref exists // to prevent — so the id is marked spent as soon as the loaded rail has been // consulted, whether or not it held a match. + // + // Applied below `objHeld`, which it asks about (objectstack#26845). const requestedSurface = useRequestedSurface(); const appliedRequestRef = React.useRef(0); - React.useEffect(() => { - if (!requestedSurface || !objectsLoaded) return; - if (requestedSurface.id === appliedRequestRef.current) return; - appliedRequestRef.current = requestedSurface.id; - const match = resolveSurfaceDeepLink(objects, requestedSurface.target, 'object'); - if (match) setCurrent(match); - }, [requestedSurface, objects, objectsLoaded]); const [objDraft, setObjDraft] = React.useState>({}); // objectui#11272 — the object `objDraft` was loaded for, `object:NAME`: // written where the load below installs it, and nowhere else. @@ -6062,6 +6064,26 @@ export function DataPillar({ const fieldCopilotMark = objLoaded && fieldSel?.kind === 'field' ? copilotDraftMarkOf(objDraftChanges, ['fields', fieldSel.id]) : null; useHeldEditReport(onHeldEditChange, objHeld?.clause ?? null); + // The live request's apply (see `requestedSurface` above). objectstack#26845 + // — another object's load replaces the buffer a held edit lives in, so it + // asks first, as the rail does; declined, the request is spent and the held + // edit stays. A request for the open object drops nothing and asks nothing. + React.useEffect(() => { + if (!requestedSurface || !objectsLoaded) return; + if (requestedSurface.id === appliedRequestRef.current) return; + appliedRequestRef.current = requestedSurface.id; + const match = resolveSurfaceDeepLink(objects, requestedSurface.target, 'object'); + if (!match) return; + if (match.name !== current?.name && !confirmDropUnsavedWork(objHeld !== null, locale)) return; + setCurrent(match); + }, [requestedSurface, objects, objectsLoaded, current, objHeld, locale]); + // objectstack#26845 — a created object opens at once, and its load replaces + // the buffer a held edit lives in: asked before the dialog opens, so Cancel + // costs nothing and the create itself asks nothing. + const openCreateObject = () => { + if (!confirmDropUnsavedWork(objHeld !== null, locale)) return; + openCreateDialog(); + }; // objectui#12725 — the header's draft line reads the badge's ledger. const objDraftStatus = useStudioItemDraftStatus( current ? { type: 'object', name: current.name } : null, @@ -6310,7 +6332,7 @@ export function DataPillar({ testId="studio-rail-data" heading={t('engine.studio.data.objects', locale)} count={objects.length} - onAdd={readOnly ? undefined : openCreateDialog} + onAdd={readOnly ? undefined : openCreateObject} addLabel={t('engine.studio.data.newObject', locale)} addTestId="studio-rail-add" search={{ value: query, onChange: setQuery, placeholder: t('engine.studio.data.searchObjects', locale) }} @@ -6435,7 +6457,7 @@ export function DataPillar({