Merge pull request #498 from open-pencil/issue-102-multifixture-audit
fix(fig): preserve nested text overrides
This commit is contained in:
commit
fece63b5b5
|
|
@ -12,6 +12,7 @@
|
|||
- Restore native copy, cut, and paste shortcuts in desktop text inputs while preserving design clipboard handling on the canvas.
|
||||
- Remove the permanent CORS configuration action from cloud-storage settings and report connection results through standard toasts with clear browser-specific guidance.
|
||||
- Complete translated app, accessibility, font, color, collaboration, import, connection-test, and browser fallback text across all supported locales, and keep the document language synchronized with the selected locale.
|
||||
- Preserve effective nested instance text overrides when importing complex Figma component hierarchies. (#102)
|
||||
- Preserve circles, ellipses, rectangles, lines, polylines, and polygons supplied as JSX children of inline SVG elements. (#452)
|
||||
|
||||
## 0.14.0 - 2026-08-10
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
sortChildren
|
||||
} from '@open-pencil/fig/node-change'
|
||||
import { initCodec, getCompiledSchema, getSchemaBytes } from '@open-pencil/kiwi/fig/codec'
|
||||
import type { NodeChange as KiwiNodeChange } from '@open-pencil/kiwi/fig/codec'
|
||||
import type { GUID, NodeChange as KiwiNodeChange } from '@open-pencil/kiwi/fig/codec'
|
||||
import { decodeBinarySchema, compileSchema, ByteBuffer } from '@open-pencil/kiwi/schema-runtime'
|
||||
import type { SceneGraph, SceneNode } from '@open-pencil/scene-graph'
|
||||
|
||||
|
|
@ -283,7 +283,9 @@ export function importClipboardNodes(
|
|||
|
||||
remapComponentIds(created, graph)
|
||||
|
||||
populateAndApplyOverrides(graph, guidMap as Map<string, InstanceNodeChange>, created, blobs)
|
||||
graph.preserveSourceMetadataDuring(() => {
|
||||
populateAndApplyOverrides(graph, guidMap as Map<string, InstanceNodeChange>, created, blobs)
|
||||
})
|
||||
|
||||
for (const figmaId of internalTopLevel) {
|
||||
const ourId = created.get(figmaId)
|
||||
|
|
@ -321,6 +323,8 @@ export async function buildFigmaClipboardHTML(
|
|||
}
|
||||
}
|
||||
|
||||
const nodeIdToGuid = new Map<string, GUID>()
|
||||
const assignedGuidValues = new Set<string>()
|
||||
const blobs: Uint8Array[] = []
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
collectTextNodes(nodes[i])
|
||||
|
|
@ -332,8 +336,12 @@ export async function buildFigmaClipboardHTML(
|
|||
localIdCounter,
|
||||
graph,
|
||||
blobs,
|
||||
nodeIdToGuid,
|
||||
fontDigestMap,
|
||||
undefined,
|
||||
fontDigestMap
|
||||
undefined,
|
||||
undefined,
|
||||
assignedGuidValues
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SceneGraph } from '@open-pencil/scene-graph'
|
||||
|
||||
import type { ProtectionMap } from '../patches'
|
||||
import { isFieldProtected, type ProtectionMap } from '../patches'
|
||||
import { buildClonesMap, syncChildrenDeep } from './clones'
|
||||
import { syncNodeProps } from './fields'
|
||||
import { indexCloneSubtree, remapRepopulatedChildSources, snapshotChildSources } from './sources'
|
||||
|
|
@ -129,6 +129,15 @@ export function propagateOverridesTransitively(
|
|||
if (!node) continue
|
||||
|
||||
if (skip.has(cloneId)) {
|
||||
// A directly overridden clone may still inherit effective text from an
|
||||
// overridden source. Respect its own text override when present.
|
||||
if (
|
||||
source.type === 'TEXT' &&
|
||||
node.type === 'TEXT' &&
|
||||
!isFieldProtected(protections, node.id, 'text')
|
||||
) {
|
||||
graph.updateNode(node.id, { text: source.text })
|
||||
}
|
||||
syncQueue.push(cloneId)
|
||||
continue
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
syncNodeProps,
|
||||
type ProtectionMap
|
||||
} from '../src/instance-overrides'
|
||||
import { propagateOverridesTransitively } from '../src/instance-overrides/sync/propagate'
|
||||
|
||||
describe('@open-pencil/fig instance interpretation', () => {
|
||||
test('populates an empty instance from its component tree', () => {
|
||||
|
|
@ -407,6 +408,33 @@ describe('@open-pencil/fig instance interpretation', () => {
|
|||
expect(graph.getNode(target.id)?.boundVariables).toEqual({ width: 'width-var' })
|
||||
})
|
||||
|
||||
test('inherits effective text on a structurally protected clone', () => {
|
||||
const graph = new SceneGraph()
|
||||
const pageId = graph.getPages()[0].id
|
||||
const component = graph.createNode('COMPONENT', pageId)
|
||||
const source = graph.createNode('TEXT', component.id, {
|
||||
text: 'Effective label'
|
||||
})
|
||||
const instance = graph.createNode('INSTANCE', pageId, { componentId: component.id })
|
||||
graph.populateInstanceChildren(instance.id, component.id, 'fig-import')
|
||||
const clone = graph.getChildren(instance.id)[0]
|
||||
graph.updateNode(clone.id, { text: 'Default label' })
|
||||
const protections: ProtectionMap = new Map()
|
||||
protectField(protections, clone.id, 'width')
|
||||
|
||||
propagateOverridesTransitively(
|
||||
graph,
|
||||
new Set([source.id, clone.id]),
|
||||
new Set(),
|
||||
new Map(),
|
||||
undefined,
|
||||
undefined,
|
||||
protections
|
||||
)
|
||||
|
||||
expect(graph.getNode(clone.id)?.text).toBe('Effective label')
|
||||
})
|
||||
|
||||
test('preserves protected text while synchronizing other fields', () => {
|
||||
const graph = new SceneGraph()
|
||||
const pageId = graph.getPages()[0].id
|
||||
|
|
|
|||
|
|
@ -141,6 +141,37 @@ describe('buildFigmaClipboardHTML', () => {
|
|||
expect(html).toContain('figmeta')
|
||||
})
|
||||
|
||||
it('preserves source metadata while importing instance overrides', async () => {
|
||||
const source = new SceneGraph()
|
||||
const sourcePage = source.getPages()[0]
|
||||
const component = source.createNode('COMPONENT', sourcePage.id, { name: 'Button' })
|
||||
source.createNode('TEXT', component.id, { name: 'Label', text: 'Effective label' })
|
||||
const instance = source.createNode('INSTANCE', sourcePage.id, {
|
||||
name: 'Button instance',
|
||||
componentId: component.id
|
||||
})
|
||||
source.populateInstanceChildren(instance.id, component.id)
|
||||
|
||||
const html = await buildFigmaClipboardHTML([component, instance], source)
|
||||
const parsed = await parseFigmaClipboard(expectDefined(html, 'Figma clipboard html'))
|
||||
const clipboard = expectDefined(parsed, 'Figma clipboard')
|
||||
const target = new SceneGraph()
|
||||
const targetPage = target.getPages()[0]
|
||||
importClipboardNodes(clipboard.nodes, target, targetPage.id)
|
||||
const importedInstance = [...target.getAllNodes()].find(
|
||||
(node) => node.type === 'INSTANCE' && node.name === 'Button instance'
|
||||
)
|
||||
const importedLabel = importedInstance
|
||||
? [...target.getAllNodes()].find(
|
||||
(node) => node.type === 'TEXT' && node.parentId === importedInstance.id
|
||||
)
|
||||
: undefined
|
||||
|
||||
expect(importedInstance).toBeDefined()
|
||||
expect(importedLabel?.text).toBe('Effective label')
|
||||
expect(importedLabel?.source.editedFields).toEqual([])
|
||||
})
|
||||
|
||||
it('roundtrips: encode then decode back', async () => {
|
||||
const graph = new SceneGraph()
|
||||
const page = graph.getPages()[0]
|
||||
|
|
|
|||
Loading…
Reference in a new issue