From 37b91729bb0bcc6b84653e768b5ff1d67ff9ffc0 Mon Sep 17 00:00:00 2001 From: Dylan Vorster Date: Thu, 9 Jul 2026 23:25:43 -0600 Subject: [PATCH] fix issue with dragging opened tray items directly into other places in the workspace --- .changeset/modern-owl-dance.md | 2 +- .changeset/polite-tigers-work.md | 7 ++++ .changeset/soft-bugs-relax.md | 7 ++++ demo/stories/RootTabSplit.stories.tsx | 33 +++++++++++++++++++ demo/stories/helpers/tools.tsx | 8 +---- .../core-models/WorkspaceCollectionModel.ts | 18 ++++++++++ .../core/src/core-models/WorkspaceModel.ts | 9 +++++ .../hooks/dnd-model/useDraggableModel.ts | 2 ++ packages/dropzone-plugin-tray/src/index.tsx | 31 ++--------------- .../src/core/FloatingWindowModel.tsx | 4 +++ packages/model-tray/src/WorkspaceTrayModel.ts | 6 ++++ 11 files changed, 91 insertions(+), 36 deletions(-) create mode 100644 .changeset/polite-tigers-work.md create mode 100644 .changeset/soft-bugs-relax.md create mode 100644 demo/stories/RootTabSplit.stories.tsx diff --git a/.changeset/modern-owl-dance.md b/.changeset/modern-owl-dance.md index 2cc2d08..e5f538c 100644 --- a/.changeset/modern-owl-dance.md +++ b/.changeset/modern-owl-dance.md @@ -2,7 +2,7 @@ '@projectstorm/react-workspaces-core': minor '@projectstorm/react-workspaces-behavior-panel-dropzone': minor '@projectstorm/react-workspaces-dropzone-plugin-tabs': minor -'@projectstorm/react-workspaces-dropzone-plugin-tray': minor +'@projectstorm/react-workspaces-dropzone-plugin-tray': patch '@projectstorm/react-workspaces-behavior-divider-dropzone': patch '@projectstorm/react-workspaces-behavior-resize': patch '@projectstorm/react-workspaces-defaults': patch diff --git a/.changeset/polite-tigers-work.md b/.changeset/polite-tigers-work.md new file mode 100644 index 0000000..d6847cb --- /dev/null +++ b/.changeset/polite-tigers-work.md @@ -0,0 +1,7 @@ +--- +'@projectstorm/react-workspaces-dropzone-plugin-tray': patch +--- + +Disable vertical split drop targets for trays while retaining tray transform targets. + +Fix moving a collapsed-tray panel into another collection so the tray releases its floating-window ownership first. diff --git a/.changeset/soft-bugs-relax.md b/.changeset/soft-bugs-relax.md new file mode 100644 index 0000000..9347037 --- /dev/null +++ b/.changeset/soft-bugs-relax.md @@ -0,0 +1,7 @@ +--- +'@projectstorm/react-workspaces-core': patch +'@projectstorm/react-workspaces-model-floating-window': patch +'@projectstorm/react-workspaces-model-tray': patch +--- + +Move the content of floating tray windows, rather than the window shell, when dropping them into another collection. Release tray ownership first to prevent stale layout state during tray-to-tab drops. diff --git a/demo/stories/RootTabSplit.stories.tsx b/demo/stories/RootTabSplit.stories.tsx new file mode 100644 index 0000000..affb3d7 --- /dev/null +++ b/demo/stories/RootTabSplit.stories.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { useState } from 'react'; +import 'typeface-open-sans'; +import { ExpandNodeModel } from '@projectstorm/react-workspaces-core'; +import { DefaultWorkspacePanelModel } from '@projectstorm/react-workspaces-defaults'; +import { WorkspaceTabModel } from '@projectstorm/react-workspaces-model-tabs'; +import { CompInternal, SharedArgs, StoryArgs, useEngine } from './helpers/tools'; + +export const RootTabSplit = Object.assign( + function RootTabSplit(args: StoryArgs) { + const engine = useEngine(args); + const [model] = useState(() => { + const tabs = new WorkspaceTabModel(); + tabs.addModel(new DefaultWorkspacePanelModel('Tab 1')); + tabs.addModel(new DefaultWorkspacePanelModel('Tab 2')); + + return new ExpandNodeModel() + .setHorizontal(true) + .addModel(new DefaultWorkspacePanelModel('Left panel')) + .addModel(tabs); + }); + + return ; + }, + { args: SharedArgs } +); + +export default { + title: 'Workspace', + parameters: { + layout: 'fullscreen' + } +}; diff --git a/demo/stories/helpers/tools.tsx b/demo/stories/helpers/tools.tsx index 3bbe778..507bb84 100644 --- a/demo/stories/helpers/tools.tsx +++ b/demo/stories/helpers/tools.tsx @@ -25,7 +25,6 @@ import { } from '@projectstorm/react-workspaces-behavior-panel-dropzone'; import { draggingItemDividerBehavior } from '@projectstorm/react-workspaces-behavior-divider-dropzone'; import { WorkspaceTabFactory } from '@projectstorm/react-workspaces-model-tabs'; -import { WorkspaceTrayModel } from '@projectstorm/react-workspaces-model-tray'; import { resizingBehavior } from '@projectstorm/react-workspaces-behavior-resize'; import { RootWorkspaceModel } from '@projectstorm/react-workspaces-model-floating-window'; import { ConvertToTabZone, getDirectiveForTabModel } from '@projectstorm/react-workspaces-dropzone-plugin-tabs'; @@ -112,12 +111,7 @@ export const useEngine = (args: StoryArgs = SharedArgs) => { engine: e, getDropZoneForModel: (model) => { return ( - getDirectiveForTrayModel( - model, - [], - () => new ExpandNodeModel(), - !(e.rootModel.flatten().find((candidate) => candidate.id === e.draggingID) instanceof WorkspaceTrayModel) - ) || + getDirectiveForTrayModel(model) || getDirectiveForWorkspaceNode({ node: model, transformZones: [ConvertToTabZone(tabFactory), ConvertToTrayZone(trayFactory)], diff --git a/packages/core/src/core-models/WorkspaceCollectionModel.ts b/packages/core/src/core-models/WorkspaceCollectionModel.ts index be7d68d..5ecfd71 100644 --- a/packages/core/src/core-models/WorkspaceCollectionModel.ts +++ b/packages/core/src/core-models/WorkspaceCollectionModel.ts @@ -130,6 +130,24 @@ export class WorkspaceCollectionModel< this.children.splice(position - 1, 0, model); } } else { + // A model can be rendered by an intermediary (for example a tray's floating + // window) while still being owned by a collection elsewhere in the tree. + // Detach that owner before re-parenting so it cannot continue to render the + // same model with stale layout dimensions. + if ( + model.parent && + (!(model.parent instanceof WorkspaceCollectionModel) || !model.parent.children.includes(model)) + ) { + const owner = model + .getRootModel() + .flatten() + .find( + (candidate) => + candidate instanceof WorkspaceCollectionModel && candidate !== this && candidate.children.includes(model) + ) as WorkspaceCollectionModel | undefined; + owner?.removeModel(model); + } + if (model.parent) { model.delete(); } diff --git a/packages/core/src/core-models/WorkspaceModel.ts b/packages/core/src/core-models/WorkspaceModel.ts index fa892f1..6539209 100644 --- a/packages/core/src/core-models/WorkspaceModel.ts +++ b/packages/core/src/core-models/WorkspaceModel.ts @@ -167,6 +167,15 @@ export class WorkspaceModel< return [this.r_dimensions]; } + /** + * Returns the model that should be inserted when this model is dropped into + * a workspace collection. Models that merely present another model can + * override this to move their content instead of their presentation shell. + */ + getDropModel(): WorkspaceModel { + return this; + } + invalidateDimensions() { this.iterateListeners((cb) => cb.dimensionsInvalidated?.()); } diff --git a/packages/core/src/widgets/hooks/dnd-model/useDraggableModel.ts b/packages/core/src/widgets/hooks/dnd-model/useDraggableModel.ts index c227e48..8caf965 100644 --- a/packages/core/src/widgets/hooks/dnd-model/useDraggableModel.ts +++ b/packages/core/src/widgets/hooks/dnd-model/useDraggableModel.ts @@ -67,6 +67,8 @@ export const useDroppableModel = (props: UseDroppableModelOptions) => { draggingNode = found || draggingNode; } + draggingNode = draggingNode.getDropModel(); + log(`workspace model dropped`, draggingNode); props.onDrop(draggingNode); }, diff --git a/packages/dropzone-plugin-tray/src/index.tsx b/packages/dropzone-plugin-tray/src/index.tsx index ca1236b..8376c19 100644 --- a/packages/dropzone-plugin-tray/src/index.tsx +++ b/packages/dropzone-plugin-tray/src/index.tsx @@ -1,10 +1,5 @@ import * as React from 'react'; -import { - Alignment, - WorkspaceCollectionModel, - WorkspaceModel, - WorkspaceNodeModel -} from '@projectstorm/react-workspaces-core'; +import { WorkspaceCollectionModel, WorkspaceModel } from '@projectstorm/react-workspaces-core'; import { DropZoneLayerButtonWidget, DropZonePanelDirective, @@ -45,32 +40,12 @@ export const ConvertToTrayZone = (trayFactory: WorkspaceTrayFactory): TransformZ export const getDirectiveForTrayModel = ( node: WorkspaceModel, - transformZones: TransformZone[] = [], - generateParentNode: () => WorkspaceNodeModel = () => new WorkspaceNodeModel(), - allowSplit: boolean = true + transformZones: TransformZone[] = [] ): DropZonePanelDirective | null => { if (!(node instanceof WorkspaceCollectionModel) && node.parent instanceof WorkspaceTrayModel) { - const tray = node.parent; - const parent = tray.parent; - const splitZones = - allowSplit && parent instanceof WorkspaceNodeModel && parent === tray.getRootModel() - ? [Alignment.TOP, Alignment.BOTTOM].map((alignment) => ({ - alignment, - handleDrop: (model: WorkspaceModel, engine) => { - const split = generateParentNode() - .setVertical(true) - .setExpand(tray.expandHorizontal, tray.expandVertical); - parent.replaceModel(tray, split); - split.addModel(tray); - split.addModel(model, alignment === Alignment.TOP ? 0 : null); - engine.normalize(); - } - })) - : []; - return { transformZones: [AppendToTrayZone, ...transformZones], - splitZones + splitZones: [] }; } }; diff --git a/packages/model-floating-window/src/core/FloatingWindowModel.tsx b/packages/model-floating-window/src/core/FloatingWindowModel.tsx index 7a5155c..ab8d4a3 100644 --- a/packages/model-floating-window/src/core/FloatingWindowModel.tsx +++ b/packages/model-floating-window/src/core/FloatingWindowModel.tsx @@ -56,6 +56,10 @@ export class FloatingWindowModel extends WorkspaceModel cb.draggableUpdated?.()); } + getDropModel(): WorkspaceModel { + return this.child || this; + } + fromArray(payload: SerializedFloatingWindowModel, engine: WorkspaceEngineInterface) { super.fromArray(payload, engine); this.dimension.update(payload.dimensions); diff --git a/packages/model-tray/src/WorkspaceTrayModel.ts b/packages/model-tray/src/WorkspaceTrayModel.ts index 5a2939f..d961e8e 100644 --- a/packages/model-tray/src/WorkspaceTrayModel.ts +++ b/packages/model-tray/src/WorkspaceTrayModel.ts @@ -183,6 +183,12 @@ export class WorkspaceTrayModel extends WorkspaceCollectionModel< } removeModel(model: WorkspaceModel): this { + // In collapsed mode the selected child is temporarily parented by the + // floating window. Remove that window before handing the child to another + // collection, otherwise the tray retains a second stale renderer for it. + if (this.floatingWindow.child === model) { + this.setFloatingModel(null); + } super.removeModel(model); if (this.selectedModel && this.selectedModel === model) { this.selectedModel = null;