From 9d4fe4e421ac2be301a3d76a0c7d7883350656a8 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 10 Sep 2026 19:16:03 +0300 Subject: [PATCH] fix(fig): preserve marker-based instance text edits on export (#648) --- CHANGELOG.md | 1 + packages/fig/src/node-change/export-node.ts | 7 ++- .../instance/text-marker-roundtrip.test.ts | 45 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/engine/io/fig/instance/text-marker-roundtrip.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c5e8e6ee..be2a0b649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ ### Fixed +- Preserve edited instance text, including cleared labels, when saving and reopening `.fig` files. - Honor `.pen` frame layout defaults and sizing and padding shorthands so imported auto-layout frames keep their computed dimensions and child positions. (#564) - Avoid macOS Keychain prompts during credential status checks and pause repeated credential access after failures until explicitly retried from Settings. diff --git a/packages/fig/src/node-change/export-node.ts b/packages/fig/src/node-change/export-node.ts index a64b1154e..358f044aa 100644 --- a/packages/fig/src/node-change/export-node.ts +++ b/packages/fig/src/node-change/export-node.ts @@ -401,12 +401,15 @@ function serializeTextOverrides( ): KiwiSymbolOverridePayload[] { const result: KiwiSymbolOverridePayload[] = [] forEachInstanceOverride(instance.instanceOverrides, (nodeId, field, value) => { - if (field !== 'text' || typeof value !== 'string' || !nodeId) return + if (field !== 'text' || !nodeId) return const target = context.graph.getNode(nodeId) if (!target || !isDescendantOf(context, nodeId, instance.id)) return const targetGuid = resolveOverrideTargetGuid(context, target, localIdCounter) if (targetGuid) - result.push({ guidPath: { guids: [targetGuid] }, textData: { characters: value } }) + result.push({ + guidPath: { guids: [targetGuid] }, + textData: { characters: typeof value === 'string' ? value : target.text } + }) }) return result } diff --git a/tests/engine/io/fig/instance/text-marker-roundtrip.test.ts b/tests/engine/io/fig/instance/text-marker-roundtrip.test.ts new file mode 100644 index 000000000..5bb632d9a --- /dev/null +++ b/tests/engine/io/fig/instance/text-marker-roundtrip.test.ts @@ -0,0 +1,45 @@ +import { expect, test } from 'bun:test' + +import { FigmaAPI } from '@open-pencil/core' +import { exportFigFile, parseFigFile } from '@open-pencil/core/io' +import { initCodec } from '@open-pencil/core/kiwi' +import { SceneGraph, getInstanceOverride } from '@open-pencil/scene-graph' + +for (const text of ['User edit', '']) { + test(`preserves API instance text edit ${JSON.stringify(text)} across save/reload`, async () => { + await initCodec() + const graph = new SceneGraph() + const api = new FigmaAPI(graph) + const component = api.createComponent() + const label = api.createText() + label.characters = 'Default' + component.appendChild(label) + const edited = component.createInstance() + edited.name = 'Edited' + const inherited = component.createInstance() + inherited.name = 'Inherited' + edited.children[0].characters = text + const raw = graph.getNode(edited.id) + if (!raw) throw new Error('Missing instance') + expect( + getInstanceOverride(raw.instanceOverrides, edited.id, edited.children[0].id, 'text') + ).toBe(true) + label.characters = 'Component edit' + graph.syncInstances(component.id) + expect(edited.children[0].characters).toBe(text) + expect(inherited.children[0].characters).toBe('Component edit') + + const bytes = await exportFigFile(graph) + const restored = await parseFigFile(bytes.buffer as ArrayBuffer) + for (const [name, expected] of [ + ['Edited', text], + ['Inherited', 'Component edit'] + ]) { + const instance = [...restored.getAllNodes()].find( + (node) => node.type === 'INSTANCE' && node.name === name + ) + if (!instance) throw new Error(`Missing ${name}`) + expect(restored.getChildren(instance.id)[0]?.text).toBe(expected) + } + }) +}