mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 23:55:20 +00:00
Let ContentKit webframes dispatch modal and URL actions (#4611)
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@gitbook/react-contentkit": patch
|
||||
---
|
||||
|
||||
Let ContentKit webframes dispatch the built-in `@ui.*` actions. Actions posted from a webframe were forwarded to the integration as a plain re-render instead of being handled, so `@ui.modal.open`, `@ui.modal.close` and `@ui.url.open` did nothing — most visibly, an integration could not close a modal from inside its own webframe.
|
||||
|
||||
Keep `null` values and class instances such as `Date` intact when resolving dynamic bindings in an action, instead of throwing or flattening them to `{}`.
|
||||
@@ -275,6 +275,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "catalog:",
|
||||
"bun-types": "catalog:",
|
||||
"react": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "catalog:",
|
||||
"bun-types": "catalog:",
|
||||
"react": "catalog:",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
@@ -31,6 +32,7 @@
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "bun test",
|
||||
"dev": "bun run build -- --watch ./src",
|
||||
"clean": "rm -rf ./dist",
|
||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
||||
|
||||
@@ -192,18 +192,21 @@ export function ContentKit<RenderContext>(props: {
|
||||
};
|
||||
}, [update, security, clientContext, current.state, current.input.context, setCurrent, render]);
|
||||
|
||||
const onSubViewAction = React.useCallback(async (action: ContentKitAction) => {
|
||||
switch (action.action) {
|
||||
case '@ui.modal.close': {
|
||||
update({
|
||||
action,
|
||||
});
|
||||
const onSubViewAction = React.useCallback(
|
||||
async (action: ContentKitAction) => {
|
||||
switch (action.action) {
|
||||
case '@ui.modal.close': {
|
||||
update({
|
||||
action,
|
||||
});
|
||||
|
||||
setSubView(null);
|
||||
break;
|
||||
setSubView(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[update]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -143,9 +143,9 @@ export function ElementWebframe(props: ContentKitClientElementProps<ContentKitWe
|
||||
}
|
||||
break;
|
||||
default:
|
||||
renderer.update({
|
||||
action: message.action,
|
||||
});
|
||||
// Go through dispatchAction so webframes can trigger the built-in
|
||||
// `@ui.*` actions, like a ContentKit button does.
|
||||
renderer.dispatchAction(message.action);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { resolveDynamicBinding } from './dynamic';
|
||||
|
||||
const state = { name: 'Nolann', count: 3, missing: undefined };
|
||||
|
||||
describe('resolveDynamicBinding', () => {
|
||||
it('resolves a binding against the state', () => {
|
||||
expect(
|
||||
resolveDynamicBinding<Record<string, unknown>>(state, { label: { $state: 'name' } })
|
||||
).toEqual({ label: 'Nolann' });
|
||||
});
|
||||
|
||||
it('resolves bindings nested in arrays', () => {
|
||||
expect(
|
||||
resolveDynamicBinding<Record<string, unknown>>(state, {
|
||||
items: [{ $state: 'count' }, 'x'],
|
||||
})
|
||||
).toEqual({ items: [3, 'x'] });
|
||||
});
|
||||
|
||||
it('passes primitives through', () => {
|
||||
expect(resolveDynamicBinding(state, 'text')).toEqual('text');
|
||||
expect(resolveDynamicBinding(state, 42)).toEqual(42);
|
||||
expect(resolveDynamicBinding(state, false)).toEqual(false);
|
||||
expect(resolveDynamicBinding(state, { a: undefined })).toEqual({ a: undefined });
|
||||
});
|
||||
|
||||
// Webframes post arbitrary JSON, so `null` is common and used to throw on the `in` check.
|
||||
it('passes null through, at any depth', () => {
|
||||
expect(resolveDynamicBinding(state, { action: 'foo', data: null })).toEqual({
|
||||
action: 'foo',
|
||||
data: null,
|
||||
});
|
||||
expect(resolveDynamicBinding(state, { items: [1, null] })).toEqual({ items: [1, null] });
|
||||
});
|
||||
|
||||
// A structured clone carries real Date instances; walking their entries would yield `{}`.
|
||||
it('leaves class instances intact', () => {
|
||||
const date = new Date('2020-01-01');
|
||||
expect(resolveDynamicBinding(state, { at: date })).toEqual({ at: date });
|
||||
});
|
||||
|
||||
it('leaves an action without any binding unchanged', () => {
|
||||
const action = { action: 'my.custom.action', props: { a: 1, b: ['x'] } };
|
||||
expect(resolveDynamicBinding(state, action)).toEqual(action);
|
||||
});
|
||||
});
|
||||
@@ -23,21 +23,20 @@ export function resolveDynamicBinding<T extends {}>(
|
||||
state: object,
|
||||
value: ContentKitDynamicBinding | T
|
||||
): T {
|
||||
if (
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean' ||
|
||||
typeof value === 'undefined'
|
||||
) {
|
||||
// Primitives
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// @ts-ignore
|
||||
return value.map((v) => resolveDynamicBinding(state, v));
|
||||
}
|
||||
|
||||
// Only an object literal can hold a binding: `'$state' in value` throws on `null`, and the
|
||||
// entries walk below would flatten a class instance (a webframe can post a `Date`) to `{}`.
|
||||
const prototype =
|
||||
typeof value === 'object' && value !== null ? Object.getPrototypeOf(value) : undefined;
|
||||
if (prototype !== Object.prototype && prototype !== null) {
|
||||
// @ts-ignore
|
||||
return value;
|
||||
}
|
||||
|
||||
if ('$state' in value && typeof value.$state === 'string') {
|
||||
// @ts-ignore
|
||||
return state[value.$state];
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true
|
||||
"incremental": true,
|
||||
"types": ["bun-types"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
Reference in New Issue
Block a user