mirror of
https://github.com/penpot/penpot.git
synced 2026-07-21 05:27:50 +00:00
🐛 Fix problems with plugins api setPluginData (#10632)
This commit is contained in:
parent
64b0bff7bd
commit
d6c50cc40b
@ -1059,6 +1059,9 @@
|
||||
:setPluginData
|
||||
(fn [key value]
|
||||
(cond
|
||||
(not= file-id (:current-file-id @st/state))
|
||||
(u/not-valid plugin-id :setPluginData-non-local-library file-id)
|
||||
|
||||
(not (string? key))
|
||||
(u/not-valid plugin-id :setPluginData-key key)
|
||||
|
||||
@ -1092,6 +1095,9 @@
|
||||
:setSharedPluginData
|
||||
(fn [namespace key value]
|
||||
(cond
|
||||
(not= file-id (:current-file-id @st/state))
|
||||
(u/not-valid plugin-id :setSharedPluginData-non-local-library file-id)
|
||||
|
||||
(not (string? namespace))
|
||||
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
|
||||
|
||||
|
||||
@ -272,13 +272,13 @@
|
||||
(st/emit! (dp/set-plugin-data file-id :page id (keyword "shared" namespace) key value))))
|
||||
|
||||
:getSharedPluginDataKeys
|
||||
(fn [self namespace]
|
||||
(fn [namespace]
|
||||
(cond
|
||||
(not (string? namespace))
|
||||
(u/not-valid plugin-id :page-plugin-data-namespace namespace)
|
||||
|
||||
:else
|
||||
(let [page (u/proxy->page self)]
|
||||
(let [page (u/locate-page file-id id)]
|
||||
(apply array (keys (dm/get-in page [:plugin-data (keyword "shared" namespace)]))))))
|
||||
|
||||
:openPage
|
||||
|
||||
@ -52,6 +52,8 @@
|
||||
- **plugins-runtime**: `penpot.openPage()` (and `Page.openPage()`) now resolves immediately when the target page is already active, instead of waiting forever for a page-initialization event that never fires.
|
||||
- **plugins-runtime**: `Shape.shadows`, `Shape.exports` and grid `rows`/`columns` now return live proxies, so writing a member on a returned shadow/export/track (e.g. `shape.shadows[0].blur = 7`) persists to the shape instead of mutating a detached snapshot that was silently discarded. The shadow `color` remains a plain snapshot (reconfigure it by assigning `shadow.color`).
|
||||
- **plugins-runtime**: Setting a variant component's `path` now renames the whole variant (its container and every main instance), like the `name` setter already did, instead of renaming only the component and leaving the file referentially inconsistent (which the backend rejected on save with a `variant-component-bad-name` error).
|
||||
- **plugins-runtime**: `Page.getSharedPluginDataKeys(namespace)` now works instead of always raising a namespace validation error: the implementation expected a spurious leading argument, so the caller's `namespace` was read as a missing second argument.
|
||||
- **plugins-runtime**: Storing plugin data on a connected (non-local) shared library is now consistently rejected with a `setPluginData-non-local-library` error on the `Library` object as well as its assets (colors, typographies, components). Previously the `Library` object accepted the write and applied it optimistically, but plugin data is not part of library synchronization and the change only persists when the caller can edit the library file — on a read-only shared library it failed silently and was lost on reload. Plugin data can only be stored on the file currently being edited.
|
||||
|
||||
## 1.4.2 (2026-01-21)
|
||||
|
||||
|
||||
@ -34,6 +34,24 @@ describe('Library', () => {
|
||||
expect(Array.isArray(ctx.penpot.library.connected)).toBe(true);
|
||||
});
|
||||
|
||||
// The Library object itself carries plugin data (it extends PluginData),
|
||||
// stored on the underlying file. Exercised on the local library; the
|
||||
// connected-library case (writing to a shared library's file/assets) shares
|
||||
// the same code path but can't be fixtured here (connecting a library hangs).
|
||||
test('local library stores plugin data', (ctx) => {
|
||||
const lib = ctx.penpot.library.local;
|
||||
lib.setPluginData('k', 'v');
|
||||
expect(lib.getPluginData('k')).toBe('v');
|
||||
expect(lib.getPluginDataKeys()).toContain('k');
|
||||
});
|
||||
|
||||
test('local library stores shared plugin data', (ctx) => {
|
||||
const lib = ctx.penpot.library.local;
|
||||
lib.setSharedPluginData('ns', 'k', 'v');
|
||||
expect(lib.getSharedPluginData('ns', 'k')).toBe('v');
|
||||
expect(lib.getSharedPluginDataKeys('ns')).toContain('k');
|
||||
});
|
||||
|
||||
test('library elements expose a libraryId', (ctx) => {
|
||||
const color = ctx.penpot.library.local.createColor();
|
||||
expect(typeof color.libraryId).toBe('string');
|
||||
@ -136,6 +154,13 @@ describe('Library', () => {
|
||||
expect(color.getPluginData('k')).toBe('v');
|
||||
expect(color.getPluginDataKeys()).toContain('k');
|
||||
});
|
||||
|
||||
test('color shared plugin data round-trips', (ctx) => {
|
||||
const color = ctx.penpot.library.local.createColor();
|
||||
color.setSharedPluginData('ns', 'k', 'v');
|
||||
expect(color.getSharedPluginData('ns', 'k')).toBe('v');
|
||||
expect(color.getSharedPluginDataKeys('ns')).toContain('k');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Typographies', () => {
|
||||
@ -170,6 +195,13 @@ describe('Library', () => {
|
||||
expect(typo.getPluginDataKeys()).toContain('k');
|
||||
});
|
||||
|
||||
test('typography shared plugin data round-trips', (ctx) => {
|
||||
const typo = ctx.penpot.library.local.createTypography();
|
||||
typo.setSharedPluginData('ns', 'k', 'v');
|
||||
expect(typo.getSharedPluginData('ns', 'k')).toBe('v');
|
||||
expect(typo.getSharedPluginDataKeys('ns')).toContain('k');
|
||||
});
|
||||
|
||||
test('typography fontFamily and fontId round-trip', (ctx) => {
|
||||
const typo = ctx.penpot.library.local.createTypography();
|
||||
expect(typeof typo.fontFamily).toBe('string');
|
||||
@ -256,6 +288,15 @@ describe('Library', () => {
|
||||
expect(comp.getPluginDataKeys()).toContain('k');
|
||||
});
|
||||
|
||||
test('component shared plugin data round-trips', (ctx) => {
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
const comp = ctx.penpot.library.local.createComponent([rect]);
|
||||
comp.setSharedPluginData('ns', 'k', 'v');
|
||||
expect(comp.getSharedPluginData('ns', 'k')).toBe('v');
|
||||
expect(comp.getSharedPluginDataKeys('ns')).toContain('k');
|
||||
});
|
||||
|
||||
test('component instance and mainInstance return shapes', (ctx) => {
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
|
||||
@ -26,6 +26,41 @@ describe('Plugin data', () => {
|
||||
if (file) {
|
||||
file.setPluginData('fileKey', 'fileValue');
|
||||
expect(file.getPluginData('fileKey')).toBe('fileValue');
|
||||
expect(file.getPluginDataKeys()).toContain('fileKey');
|
||||
}
|
||||
});
|
||||
|
||||
test('shared plugin data round-trips on the file', (ctx) => {
|
||||
const file = ctx.penpot.currentFile;
|
||||
expect(file).not.toBeNull();
|
||||
if (file) {
|
||||
file.setSharedPluginData('ns', 'fileShared', 'fileSharedValue');
|
||||
expect(file.getSharedPluginData('ns', 'fileShared')).toBe(
|
||||
'fileSharedValue',
|
||||
);
|
||||
expect(file.getSharedPluginDataKeys('ns')).toContain('fileShared');
|
||||
}
|
||||
});
|
||||
|
||||
test('plugin data round-trips on a page', (ctx) => {
|
||||
const page = ctx.penpot.currentPage;
|
||||
expect(page).not.toBeNull();
|
||||
if (page) {
|
||||
page.setPluginData('pageKey', 'pageValue');
|
||||
expect(page.getPluginData('pageKey')).toBe('pageValue');
|
||||
expect(page.getPluginDataKeys()).toContain('pageKey');
|
||||
}
|
||||
});
|
||||
|
||||
test('shared plugin data round-trips on a page', (ctx) => {
|
||||
const page = ctx.penpot.currentPage;
|
||||
expect(page).not.toBeNull();
|
||||
if (page) {
|
||||
page.setSharedPluginData('ns', 'pageShared', 'pageSharedValue');
|
||||
expect(page.getSharedPluginData('ns', 'pageShared')).toBe(
|
||||
'pageSharedValue',
|
||||
);
|
||||
expect(page.getSharedPluginDataKeys('ns')).toContain('pageShared');
|
||||
}
|
||||
});
|
||||
|
||||
@ -45,6 +80,47 @@ describe('Plugin data', () => {
|
||||
expect(rect.getPluginDataKeys()).toContain('');
|
||||
});
|
||||
|
||||
// Keys are opaque strings: no case normalization is applied. A camelCase key
|
||||
// and its kebab-case spelling are distinct entries that each round-trip
|
||||
// independently. Pins that behaviour so a future key-normalization regression
|
||||
// (reported as camelCase keys "behaving incorrectly") would be caught here.
|
||||
test('camelCase and kebab-case keys are distinct and both round-trip', (ctx) => {
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
rect.setPluginData('myKey', 'camel');
|
||||
rect.setPluginData('my-key', 'kebab');
|
||||
expect(rect.getPluginData('myKey')).toBe('camel');
|
||||
expect(rect.getPluginData('my-key')).toBe('kebab');
|
||||
const keys = rect.getPluginDataKeys();
|
||||
expect(keys).toContain('myKey');
|
||||
expect(keys).toContain('my-key');
|
||||
});
|
||||
|
||||
// Same guarantee at the file-data storage location (a distinct code path from
|
||||
// shape/page objects), covering a camelCase key on the file itself.
|
||||
test('a camelCase key round-trips on the file', (ctx) => {
|
||||
const file = ctx.penpot.currentFile;
|
||||
expect(file).not.toBeNull();
|
||||
if (file) {
|
||||
file.setPluginData('camelCaseFileKey', 'value');
|
||||
expect(file.getPluginData('camelCaseFileKey')).toBe('value');
|
||||
expect(file.getPluginDataKeys()).toContain('camelCaseFileKey');
|
||||
}
|
||||
});
|
||||
|
||||
// camelCase is likewise preserved in the shared namespace and the shared key.
|
||||
test('camelCase shared namespace and key round-trip', (ctx) => {
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
rect.setSharedPluginData('myNamespace', 'myKey', 'camel');
|
||||
rect.setSharedPluginData('myNamespace', 'my-key', 'kebab');
|
||||
expect(rect.getSharedPluginData('myNamespace', 'myKey')).toBe('camel');
|
||||
expect(rect.getSharedPluginData('myNamespace', 'my-key')).toBe('kebab');
|
||||
const keys = rect.getSharedPluginDataKeys('myNamespace');
|
||||
expect(keys).toContain('myKey');
|
||||
expect(keys).toContain('my-key');
|
||||
});
|
||||
|
||||
test('setPluginData with a non-string value throws', (ctx) => {
|
||||
const rect = ctx.penpot.createRectangle();
|
||||
ctx.board.appendChild(rect);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user