diff --git a/code/CHANGELOG.md b/code/CHANGELOG.md index cb263bfc..f4a8cc08 100644 --- a/code/CHANGELOG.md +++ b/code/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- [#1112](https://github.com/InditexTech/weavejs/issues/1112) Preserve Node IDs When Grouping Nodes + ## [5.0.0] - 2026-06-18 ### Added diff --git a/code/packages/react/src/components/provider.tsx b/code/packages/react/src/components/provider.tsx index 791cb774..00c65883 100644 --- a/code/packages/react/src/components/provider.tsx +++ b/code/packages/react/src/components/provider.tsx @@ -10,6 +10,7 @@ import type { WeaveRenderer, WeaveStore, WeavePerformanceConfig, + WeaveFontsPreloadFunction, } from '@inditextech/weave-sdk'; import { Weave } from '@inditextech/weave-sdk'; import { @@ -25,7 +26,7 @@ import { useWeave } from './store'; type WeaveProviderType = { getContainer: () => HTMLElement; - fonts?: WeaveFont[] | (() => Promise); + fonts?: WeaveFont[] | WeaveFontsPreloadFunction; store: WeaveStore; renderer: WeaveRenderer; nodes?: WeaveNode[]; diff --git a/code/packages/sdk/src/actions/images-tool/__tests__/images-tool.test.ts b/code/packages/sdk/src/actions/images-tool/__tests__/images-tool.test.ts index 04e0cfd3..23671d2b 100644 --- a/code/packages/sdk/src/actions/images-tool/__tests__/images-tool.test.ts +++ b/code/packages/sdk/src/actions/images-tool/__tests__/images-tool.test.ts @@ -45,6 +45,7 @@ vi.mock('@/utils/utils', () => ({ mergeExceptArrays: vi.fn( (a: Record, b: Record) => ({ ...a, ...b }) ), + sleep: vi.fn().mockResolvedValue(undefined), })); if (typeof (globalThis as Record)['window'] === 'undefined') { @@ -395,8 +396,8 @@ describe('WeaveImagesToolAction', () => { expect((action as unknown as R)['uploadImageFunction']).toBe(uploadFn); }); - it('5.2 nodesIds reset to [] inside FILE block', () => { - (action as unknown as R)['nodesIds'] = ['old-id']; + it('5.2 nodesIds reset to new Set() inside FILE block', () => { + (action as unknown as R)['nodesIds'] = new Set(['old-id']); action.trigger(vi.fn(), { type: WEAVE_IMAGES_TOOL_UPLOAD_TYPE.FILE, images: [makeImageFile()], @@ -404,9 +405,8 @@ describe('WeaveImagesToolAction', () => { onStartUploading: onStart, onFinishedUploading: onFinish, }); - // nodesIds is reset in FILE block then populated by addImages - // immediately after trigger, addImages is called but async; nodesIds may be [] - expect(Array.isArray((action as unknown as R)['nodesIds'])).toBe(true); + // nodesIds is reset to a new Set in FILE block then populated by addImages + expect((action as unknown as R)['nodesIds']).toBeInstanceOf(Set); }); it('5.3 addImages called → state transitions toward DEFINING_POSITION', async () => { @@ -897,8 +897,8 @@ describe('WeaveImagesToolAction', () => { it('13.1 nodeId in nodesIds → handleImageAdded called', async () => { const checkAdded = await setupHandleAdding(); - const nodesIds = (action as unknown as R)['nodesIds'] as string[]; - const nodeId = nodesIds[0]; + const nodesIds = (action as unknown as R)['nodesIds'] as Set; + const nodeId = Array.from(nodesIds)[0]; (action as unknown as R)['toAdd'] = 5; // keep > 0 so cleanup doesn't happen const handleImageAddedSpy = vi.spyOn(action, 'handleImageAdded'); checkAdded?.({ nodeId }); @@ -915,14 +915,14 @@ describe('WeaveImagesToolAction', () => { it('13.3 getImagesAdded()<=0 → setState(FINISHED) + cancelAction + removeEventListener + emitEvent', async () => { const checkAdded = await setupHandleAdding(); - const nodesIds = (action as unknown as R)['nodesIds'] as string[]; + const nodesIds = (action as unknown as R)['nodesIds'] as Set; (action as unknown as R)['toAdd'] = 1; // will become 0 after handleImageAdded const cancelFn = (action as unknown as R)['cancelAction'] as ReturnType; - checkAdded?.({ nodeId: nodesIds[0] }); + checkAdded?.({ nodeId: Array.from(nodesIds)[0] }); expect((action as unknown as R)['state']).toBe(WEAVE_IMAGES_TOOL_STATE.FINISHED); expect(cancelFn).toHaveBeenCalled(); expect(mockWeave.removeEventListener).toHaveBeenCalledWith('onAddedImage', expect.any(Function)); - expect(mockWeave.emitEvent).toHaveBeenCalledWith('onAddedImages', { nodesIds }); + expect(mockWeave.emitEvent).toHaveBeenCalledWith('onAddedImages', { nodesIds: Array.from(nodesIds) }); }); it('13.4 getImagesAdded()>0 → no setState/cancelAction', async () => { @@ -1010,7 +1010,7 @@ describe('WeaveImagesToolAction', () => { describe('Suite 16: cleanup', () => { beforeEach(() => { setupAction(); - (action as unknown as R)['nodesIds'] = ['node-1', 'node-2']; + (action as unknown as R)['nodesIds'] = new Set(['node-1', 'node-2']); }); it('16.1 tempPointerFeedbackNode present → destroy + null', () => { @@ -1078,7 +1078,7 @@ describe('WeaveImagesToolAction', () => { expect((action as unknown as R)['initialCursor']).toBeNull(); expect((action as unknown as R)['container']).toBeUndefined(); expect((action as unknown as R)['clickPoint']).toBeNull(); - expect((action as unknown as R)['nodesIds']).toEqual([]); + expect((action as unknown as R)['nodesIds']).toEqual(new Set()); expect((action as unknown as R)['toAdd']).toBe(0); expect((action as unknown as R)['state']).toBe(WEAVE_IMAGES_TOOL_STATE.IDLE); }); diff --git a/code/packages/sdk/src/actions/images-tool/images-tool.ts b/code/packages/sdk/src/actions/images-tool/images-tool.ts index 91f7b404..d81c837b 100644 --- a/code/packages/sdk/src/actions/images-tool/images-tool.ts +++ b/code/packages/sdk/src/actions/images-tool/images-tool.ts @@ -54,7 +54,7 @@ export class WeaveImagesToolAction extends WeaveAction { protected pointers!: Map; protected tempPointerFeedbackNode!: Konva.Group | null; protected container: Konva.Layer | Konva.Group | undefined; - protected nodesIds: string[] = []; + protected nodesIds: Set = new Set(); private toAdd: number = 0; protected imagesFile: WeaveImagesFile[] = []; protected imagesURL: WeaveImagesURL[] = []; @@ -412,11 +412,11 @@ export class WeaveImagesToolAction extends WeaveAction { let imagePositionY = originPoint.y; let maxHeight = 0; - this.nodesIds = []; + this.nodesIds = new Set(); const images: Promise[] = []; const checkAddedImages = ({ nodeId }: { nodeId: string }) => { - if (this.nodesIds.includes(nodeId)) { + if (this.nodesIds.has(nodeId)) { this.handleImageAdded(); } @@ -429,7 +429,7 @@ export class WeaveImagesToolAction extends WeaveAction { this.instance.emitEvent( 'onAddedImages', - { nodesIds: this.nodesIds } + { nodesIds: Array.from(this.nodesIds) } ); } }; @@ -509,7 +509,7 @@ export class WeaveImagesToolAction extends WeaveAction { handleImage(nodeId, image, { x: imagePositionX, y: imagePositionY }) ); - this.nodesIds.push(nodeId); + this.nodesIds.add(nodeId); maxHeight = Math.max(maxHeight, height); @@ -564,7 +564,7 @@ export class WeaveImagesToolAction extends WeaveAction { handleImage(nodeId, image, { x: imagePositionX, y: imagePositionY }) ); - this.nodesIds.push(nodeId); + this.nodesIds.add(nodeId); maxHeight = Math.max(maxHeight, image.height); @@ -577,8 +577,8 @@ export class WeaveImagesToolAction extends WeaveAction { } } - if (this.nodesIds.length > 0) { - this.toAdd = this.nodesIds.length; + if (this.nodesIds.size > 0) { + this.toAdd = this.nodesIds.size; await Promise.allSettled(images); } else { @@ -616,7 +616,7 @@ export class WeaveImagesToolAction extends WeaveAction { this.container = params.container; } - this.nodesIds = []; + this.nodesIds = new Set(); this.forceMainContainer = params.forceMainContainer ?? false; if (params.type === WEAVE_IMAGES_TOOL_UPLOAD_TYPE.FILE) { @@ -624,13 +624,13 @@ export class WeaveImagesToolAction extends WeaveAction { this.onStartUploading = params.onStartUploading; this.onFinishedUploading = params.onFinishedUploading; this.uploadImageFunction = params.uploadImageFunction; - this.nodesIds = []; + this.nodesIds = new Set(); this.imagesFile = params.images; } if (params.type === WEAVE_IMAGES_TOOL_UPLOAD_TYPE.IMAGE_URL) { this.uploadType = WEAVE_IMAGES_TOOL_UPLOAD_TYPE.IMAGE_URL; - this.nodesIds = []; + this.nodesIds = new Set(); this.imagesURL = params.images; } @@ -665,19 +665,16 @@ export class WeaveImagesToolAction extends WeaveAction { } } - cleanup(): void { + async waitForImagesToBeAdded(nodesIds: Set) { const stage = this.instance.getStage(); - this.tempPointerFeedbackNode?.destroy(); - this.tempPointerFeedbackNode = null; - this.instance.getUtilityLayer()?.batchDraw(); - const selectionPlugin = this.instance.getPlugin('nodesSelection'); + if (selectionPlugin) { const addedNodes = []; - for (const nodeId of this.nodesIds) { + for (const nodeId of Array.from(nodesIds)) { const node = stage.findOne(`#${nodeId}`); if (node) { @@ -688,6 +685,19 @@ export class WeaveImagesToolAction extends WeaveAction { selectionPlugin.setSelectedNodes(addedNodes); this.instance.triggerAction(SELECTION_TOOL_ACTION_NAME); } + } + + cleanup(): void { + const stage = this.instance.getStage(); + + this.tempPointerFeedbackNode?.destroy(); + this.tempPointerFeedbackNode = null; + this.instance.getUtilityLayer()?.batchDraw(); + + const nodesIds = this.nodesIds; + this.nodesIds = new Set(); + this.toAdd = 0; + this.waitForImagesToBeAdded(nodesIds); this.instance.emitEvent('onFinishedImages'); @@ -701,8 +711,6 @@ export class WeaveImagesToolAction extends WeaveAction { this.container = undefined; this.stageClickPoint = null; this.clickPoint = null; - this.nodesIds = []; - this.toAdd = 0; this.setState(WEAVE_IMAGES_TOOL_STATE.IDLE); } diff --git a/code/packages/sdk/src/managers/__tests__/groups.test.ts b/code/packages/sdk/src/managers/__tests__/groups.test.ts index efa22b66..1ec860e9 100644 --- a/code/packages/sdk/src/managers/__tests__/groups.test.ts +++ b/code/packages/sdk/src/managers/__tests__/groups.test.ts @@ -388,11 +388,11 @@ describe('WeaveGroupsManager', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); - it('empty nodes: group() does not crash and calls removeNodes with []', () => { + it('empty nodes: group() does not crash and does not call removeNodeNT', () => { const { weave } = makeMockWeave(); const mgr = new WeaveGroupsManager(weave); mgr.group([]); - expect(weave.removeNodes).toHaveBeenCalledWith([]); + expect(weave.removeNodeNT).not.toHaveBeenCalled(); }); it('no frames: realNodes = nodes, all nodes are processed', () => { @@ -448,27 +448,35 @@ describe('WeaveGroupsManager', () => { }); const mgr = new WeaveGroupsManager(weave); mgr.group([frameEl, childEl, otherEl]); - // childEl should be excluded; removeNodes should not contain childEl - expect(weave.removeNodes).toHaveBeenCalledWith( - expect.not.arrayContaining([childEl]) - ); + // childEl should be excluded from realNodes; removeNodeNT should not be called with it + expect(weave.removeNodeNT).not.toHaveBeenCalledWith(childEl, expect.anything()); }); it('frame child NOT excluded when parent has no nodeId in framesIds', () => { const mainLayer = new Konva.Layer(); const frameKonva = new Konva.Group({ nodeType: 'frame' }); - // Child whose parent has nodeId NOT in framesIds → should be included + // Child whose parent has nodeId NOT in framesIds → should be included. + // Give childKonva an explicit id so mainLayer.findOne('#child-key') resolves + // correctly (Konva assigns empty-string ids in jsdom without an explicit id). + // The stage mock proxy simulates childKonva having parentWithOtherNodeId + // as its parent, which is what allNodesInSameParent uses for filtering. const parentWithOtherNodeId = new Konva.Group({ nodeId: 'other-id' }); - const childKonva = new Konva.Rect({ nodeType: 'rect' }); - parentWithOtherNodeId.add(childKonva); + const childKonva = new Konva.Rect({ id: 'child-key', nodeType: 'rect' }); mainLayer.add(frameKonva); + mainLayer.add(childKonva); const frameEl = { key: 'frame-key', type: 'node', props: { id: 'frame-key', nodeType: 'frame' } } as unknown as WeaveStateElement; - const childEl = { key: childKonva.id(), type: 'node', props: { id: childKonva.id(), nodeType: 'rect' } } as unknown as WeaveStateElement; + const childEl = { key: 'child-key', type: 'node', props: { id: 'child-key', nodeType: 'rect' } } as unknown as WeaveStateElement; - const stageMap: Record = { + // Proxy for childKonva in the stage mock: simulates being a child of + // parentWithOtherNodeId (nodeId='other-id', which is NOT in framesIds) + const childKonvaProxy = { + getAttrs: () => ({ nodeType: 'rect' }), + getParent: () => parentWithOtherNodeId, + }; + const stageMap: Record = { 'frame-key': frameKonva, - [childKonva.id()]: childKonva, + 'child-key': childKonvaProxy as unknown as Konva.Node, }; const groupHandler = makeGroupHandler(); const nodeHandler = makeNodeHandler(); @@ -480,9 +488,7 @@ describe('WeaveGroupsManager', () => { const mgr = new WeaveGroupsManager(weave); mgr.group([frameEl, childEl]); // childEl should be included (parent.nodeId = 'other-id' not in framesIds) - expect(weave.removeNodes).toHaveBeenCalledWith( - expect.arrayContaining([childEl]) - ); + expect(weave.removeNodeNT).toHaveBeenCalledWith(childEl, expect.anything()); }); it('single parent with nodeId → parentId = nodeId used in addNodeNT', () => { diff --git a/code/packages/sdk/src/managers/groups.ts b/code/packages/sdk/src/managers/groups.ts index df3ee0a7..3bc29819 100644 --- a/code/packages/sdk/src/managers/groups.ts +++ b/code/packages/sdk/src/managers/groups.ts @@ -16,10 +16,8 @@ import { type Logger } from 'pino'; import { WeaveNodesSelectionPlugin } from '@/plugins/nodes-selection/nodes-selection'; import type { WeaveNode } from '@/nodes/node'; import type { WeaveGroupNode } from '@/nodes/group/group'; -import { - WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_KEY, - WeaveNodesMultiSelectionFeedbackPlugin, -} from '@/index.node'; +import type { WeaveNodesMultiSelectionFeedbackPlugin } from '@/plugins/nodes-multi-selection-feedback/nodes-multi-selection-feedback'; +import { WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_KEY } from '@/plugins/nodes-multi-selection-feedback/constants'; export class WeaveGroupsManager { private instance: Weave; @@ -167,17 +165,22 @@ export class WeaveGroupsManager { const nodePos = konvaGroup.getAbsolutePosition(); const nodeRotation = konvaGroup.getAbsoluteRotation(); + const newId = uuidv4(); konvaGroup.moveTo(groupInstance); konvaGroup.setAbsolutePosition(nodePos); konvaGroup.rotation(nodeRotation); konvaGroup.zIndex(index); - konvaGroup.setAttr('id', uuidv4()); + konvaGroup.setAttr('id', newId); konvaGroup.setAttr('draggable', false); const handler = this.instance.getNodeHandler('group'); if (handler) { + this.instance.removeNodeNT(groupChild, { + emitUserChangeEvent: false, + }); + konvaGroup.setAttr('id', groupChild.key); const stateNode = handler.serialize(konvaGroup); this.instance.addNodeNT(stateNode, groupId, { emitUserChangeEvent: false, @@ -194,11 +197,12 @@ export class WeaveGroupsManager { const nodePos = konvaNode.getAbsolutePosition(); const nodeRotation = konvaNode.getAbsoluteRotation(); + const newId = uuidv4(); konvaNode.moveTo(groupInstance); konvaNode.setAbsolutePosition(nodePos); konvaNode.rotation(nodeRotation); konvaNode.zIndex(index); - konvaNode.setAttr('id', uuidv4()); + konvaNode.setAttr('id', newId); konvaNode.setAttr('draggable', false); const handler = this.instance.getNodeHandler( @@ -206,6 +210,10 @@ export class WeaveGroupsManager { ); if (handler) { + this.instance.removeNodeNT(node, { + emitUserChangeEvent: false, + }); + konvaNode.setAttr('id', node.key); const stateNode = handler.serialize(konvaNode); this.instance.addNodeNT(stateNode, groupId, { emitUserChangeEvent: false, @@ -214,8 +222,6 @@ export class WeaveGroupsManager { } } - this.instance.removeNodes(sortedNodesByZIndex); - groupInstance.destroy(); const groupNode = stage.findOne(`#${groupId}`); @@ -243,7 +249,7 @@ export class WeaveGroupsManager { tr.show(); tr.forceUpdate(); } - }, 0); + }, 10); }); } @@ -397,7 +403,7 @@ export class WeaveGroupsManager { }; } - getNodesMultiSelectionFeedbackPlugin() { + getNodesMultiSelectionFeedbackPlugin(): WeaveNodesMultiSelectionFeedbackPlugin | undefined { return this.instance.getPlugin( WEAVE_NODES_MULTI_SELECTION_FEEDBACK_PLUGIN_KEY ); diff --git a/code/packages/sdk/src/nodes/image/__tests__/image.test.ts b/code/packages/sdk/src/nodes/image/__tests__/image.test.ts index 4b4c9b98..f1e78f12 100644 --- a/code/packages/sdk/src/nodes/image/__tests__/image.test.ts +++ b/code/packages/sdk/src/nodes/image/__tests__/image.test.ts @@ -608,18 +608,94 @@ describe('WeaveImageNode', () => { }); describe('11 — onRender: image state branches', () => { - it('11.1 preloaded image removes placeholder and shows image', () => { + it('11.1 fully-loaded image (status=loaded) removes placeholder and shows image', () => { const { node } = makeNode(); const privateNode = getPrivateNode(node); privateNode.imageSource = { 'test-image': makeMockImageElement() as unknown as HTMLImageElement, }; + // Must also have status='loaded' for the image to be shown + privateNode.imageState = { + 'test-image': { status: 'loaded', loaded: true, error: false }, + }; + + const group = node.onRender(defaultProps()) as Konva.Group; + const internalImage = group.findOne('#test-image-image') as Konva.Image; + + expect(group.findOne('#test-image-placeholder')).toBeFalsy(); + expect(internalImage.getAttrs().visible).toBe(true); + }); + + it('11.1b imageSource set but not yet loaded (status=loading) — keeps placeholder visible, does NOT show the unloaded image', () => { + // Regression: grouping images while they are downloading caused onRender to + // treat a still-loading HTMLImageElement as a fully loaded image, destroying the + // placeholder and rendering a transparent/blank node. + const { node } = makeNode(); + const privateNode = getPrivateNode(node); + const loadSpy = vi.spyOn(privateNode, 'loadImage'); + + // imageSource is set (loading started) but onload has not fired yet + privateNode.imageSource = { + 'test-image': makeMockImageElement() as unknown as HTMLImageElement, + }; + privateNode.imageState = { + 'test-image': { status: 'loading', loaded: false, error: false }, + }; + + const group = node.onRender(defaultProps()) as Konva.Group; + const placeholder = group.findOne('#test-image-placeholder'); + const internalImage = group.findOne('#test-image-image') as Konva.Image; + + // Placeholder must still be present (image not done loading) + expect(placeholder).toBeTruthy(); + // internalImage must NOT be visible + expect(internalImage.getAttrs().visible).toBeFalsy(); + // imageState must not be overwritten to 'loaded' + expect(privateNode.imageState['test-image']?.status).not.toBe('loaded'); + // loadImage should be called to restart loading on the new node + expect(loadSpy).toHaveBeenCalled(); + }); + + it('11.1c fallback showing + imageURL set + status=loading (e.g. after grouping mid-download) — shows fallback and restarts load', () => { + // Regression: when images with a thumbnail were grouped while still downloading + // the real image, the fallback/thumbnail disappeared and the image went transparent. + const fallback = makeMockImageElement() as unknown as HTMLImageElement; + const { node } = makeNode({ + imageFallback: { + enabled: true, + getId: () => 'test-image', + getDataURL: () => '', + onPersist: () => {}, + }, + }); + const privateNode = getPrivateNode(node); + const loadSpy = vi.spyOn(privateNode, 'loadImage'); + + privateNode.imageFallback = { 'test-image': fallback }; + privateNode.imageSource = { + 'test-image': makeMockImageElement() as unknown as HTMLImageElement, + }; + // URL is set but image is in loading-with-fallback state + privateNode.imageState = { + 'test-image': { status: 'loading', loaded: true, error: false }, + }; const group = node.onRender(defaultProps()) as Konva.Group; const internalImage = group.findOne('#test-image-image') as Konva.Image; + // Placeholder should be gone — fallback is shown instead expect(group.findOne('#test-image-placeholder')).toBeFalsy(); + // Fallback image must be the visible source + expect(internalImage.getAttrs().image).toBe(fallback); expect(internalImage.getAttrs().visible).toBe(true); + // imageState must remain 'loading' (not prematurely promoted to 'loaded') + expect(privateNode.imageState['test-image']?.status).toBe('loading'); + // loadImage should be restarted on the new node so the real image eventually loads + expect(loadSpy).toHaveBeenCalledWith( + expect.objectContaining({ imageURL: 'http://example.com/image.jpg' }), + group, + false + ); }); it('11.2 fallback not used when imageFallback.enabled is false (default)', () => { diff --git a/code/packages/sdk/src/nodes/image/image.ts b/code/packages/sdk/src/nodes/image/image.ts index 375c0afb..02233f4c 100644 --- a/code/packages/sdk/src/nodes/image/image.ts +++ b/code/packages/sdk/src/nodes/image/image.ts @@ -305,6 +305,7 @@ export class WeaveImageNode extends WeaveNode { const groupImageProps = { ...imageProps, }; + delete groupImageProps.name; delete groupImageProps.children; delete groupImageProps.imageProperties; delete groupImageProps.zIndex; @@ -443,9 +444,23 @@ export class WeaveImageNode extends WeaveNode { } }; - const hasFinalImageLoaded = this.imageSource[id] && imageProps.imageURL; + // An image is considered fully loaded only when its state explicitly reflects + // a successful load. imageSource[id] being set is not sufficient because + // the HTMLImageElement is created at the start of loading (before onload fires). + const isImageFullyLoaded = + this.imageState[id]?.status === 'loaded' && + this.imageState[id]?.loaded === true && + !this.imageState[id]?.error; + + const hasFinalImageLoaded = + this.imageSource[id] && imageProps.imageURL && isImageFullyLoaded; + + // Show fallback whenever the final image is not yet fully loaded and a + // fallback is available. This covers two cases: + // 1. imageURL not yet set (upload in progress, thumbnail available). + // 2. imageURL is set but the image is still downloading with a thumbnail. const hasFallbackAndFinalImageNotLoaded = - !imageProps.imageURL && + !isImageFullyLoaded && this.imageFallback[id] !== undefined && this.config.imageFallback.enabled; @@ -490,6 +505,18 @@ export class WeaveImageNode extends WeaveNode { error: false, }; + // When the fallback is showing but a real URL is available (e.g. the node + // was re-rendered during grouping while the real image was still loading), + // the previous loadImage closure held references to the now-destroyed Konva + // node. Restart loading on this new node so the final image still appears. + if (hasFallbackAndFinalImageNotLoaded && imageProps.imageURL) { + this.loadImage(imageProps, image, false); + } + + if (hasFallbackAndFinalImageNotLoaded && !imageProps.imageURL) { + this.loadImage(imageProps, image, this.config.imageFallback.enabled); + } + this.updateImageCrop(image); } else { this.updatePlaceholderSize(image); diff --git a/code/packages/sdk/src/utils/utils.ts b/code/packages/sdk/src/utils/utils.ts index 95108447..c768c51b 100644 --- a/code/packages/sdk/src/utils/utils.ts +++ b/code/packages/sdk/src/utils/utils.ts @@ -303,7 +303,8 @@ export function buildAncestorGroupIds( stage: Konva.Stage ): string[] { const ids: string[] = []; - let cur: Konva.Node | null = (stage.findOne(`#${groupId}`) as Konva.Node | null) ?? null; + let cur: Konva.Node | null = + (stage.findOne(`#${groupId}`) as Konva.Node | null) ?? null; while (cur) { const id = cur.getAttrs().id; if (id && cur.getAttrs().nodeType) ids.push(id); @@ -321,7 +322,8 @@ export function getTargetedNode(instance: Weave): Konva.Node | undefined { if (inter) { const selectionPlugin = instance.getPlugin('nodesSelection'); - const activeGroupContext = selectionPlugin?.getActiveGroupContext() ?? undefined; + const activeGroupContext = + selectionPlugin?.getActiveGroupContext() ?? undefined; // When in group context, pass the full ancestor ID set so nodes in // ancestor groups resolve to themselves (not their containing group) const stopIds: string | string[] | undefined = activeGroupContext @@ -699,3 +701,7 @@ export function getStageClickPoint( export function isNumber(value: unknown): value is number { return typeof value === 'number' && !Number.isNaN(value); } + +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/docs/content/docs/main/changelog/5.x/5.1.0.mdx b/docs/content/docs/main/changelog/5.x/5.1.0.mdx new file mode 100644 index 00000000..457299e8 --- /dev/null +++ b/docs/content/docs/main/changelog/5.x/5.1.0.mdx @@ -0,0 +1,12 @@ +--- +title: v5.1.0 +description: When grouping preserve the nodes ids +--- + +## Metadata + +- **Release date**: 2026-06-19 + +### Changed + +- [#1112](https://github.com/InditexTech/weavejs/issues/1112) Preserve Node IDs When Grouping Nodes diff --git a/docs/content/docs/main/changelog/5.x/meta.json b/docs/content/docs/main/changelog/5.x/meta.json index 76209109..e0077472 100644 --- a/docs/content/docs/main/changelog/5.x/meta.json +++ b/docs/content/docs/main/changelog/5.x/meta.json @@ -1,5 +1,5 @@ { "title": "5.x versions", "description": "Detailed changelog for Weave.js 5.x versions", - "pages": ["5.0.0"] + "pages": ["5.1.0", "5.0.0"] } diff --git a/docs/content/docs/main/changelog/index.mdx b/docs/content/docs/main/changelog/index.mdx index 2286e42b..06801fa5 100644 --- a/docs/content/docs/main/changelog/index.mdx +++ b/docs/content/docs/main/changelog/index.mdx @@ -5,6 +5,7 @@ description: Check out the latest changes to Weave.js. ## 5.x versions +- [**5.1.0**](/docs/main/changelog/5.x/5.1.0) - [**5.0.0**](/docs/main/changelog/5.x/5.0.0) ## 4.x versions