mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +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": {
|
"devDependencies": {
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
|
"bun-types": "catalog:",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"tsdown": "catalog:",
|
"tsdown": "catalog:",
|
||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "catalog:",
|
"@types/react": "catalog:",
|
||||||
|
"bun-types": "catalog:",
|
||||||
"react": "catalog:",
|
"react": "catalog:",
|
||||||
"tsdown": "catalog:",
|
"tsdown": "catalog:",
|
||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsdown",
|
"build": "tsdown",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
|
"unit": "bun test",
|
||||||
"dev": "bun run build -- --watch ./src",
|
"dev": "bun run build -- --watch ./src",
|
||||||
"clean": "rm -rf ./dist",
|
"clean": "rm -rf ./dist",
|
||||||
"publish-to-npm": "../../scripts/publish-if-new.sh"
|
"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]);
|
}, [update, security, clientContext, current.state, current.input.context, setCurrent, render]);
|
||||||
|
|
||||||
const onSubViewAction = React.useCallback(async (action: ContentKitAction) => {
|
const onSubViewAction = React.useCallback(
|
||||||
switch (action.action) {
|
async (action: ContentKitAction) => {
|
||||||
case '@ui.modal.close': {
|
switch (action.action) {
|
||||||
update({
|
case '@ui.modal.close': {
|
||||||
action,
|
update({
|
||||||
});
|
action,
|
||||||
|
});
|
||||||
|
|
||||||
setSubView(null);
|
setSubView(null);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}, []);
|
[update]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -143,9 +143,9 @@ export function ElementWebframe(props: ContentKitClientElementProps<ContentKitWe
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
renderer.update({
|
// Go through dispatchAction so webframes can trigger the built-in
|
||||||
action: message.action,
|
// `@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,
|
state: object,
|
||||||
value: ContentKitDynamicBinding | T
|
value: ContentKitDynamicBinding | T
|
||||||
): T {
|
): T {
|
||||||
if (
|
|
||||||
typeof value === 'string' ||
|
|
||||||
typeof value === 'number' ||
|
|
||||||
typeof value === 'boolean' ||
|
|
||||||
typeof value === 'undefined'
|
|
||||||
) {
|
|
||||||
// Primitives
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return value.map((v) => resolveDynamicBinding(state, v));
|
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') {
|
if ('$state' in value && typeof value.$state === 'string') {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return state[value.$state];
|
return state[value.$state];
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"incremental": true
|
"incremental": true,
|
||||||
|
"types": ["bun-types"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
|
|||||||
Reference in New Issue
Block a user