Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add decorators #5205

Merged
merged 31 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
02be55b
Add decorators
OEvgeny Jun 6, 2024
0bf63b0
Fix lint
OEvgeny Jun 6, 2024
4fa4d16
Rework into a single ActivityBorder middleware stack
OEvgeny Jun 10, 2024
662588f
Add test
OEvgeny Jun 10, 2024
38918fe
Add William's suggestions
OEvgeny Jun 12, 2024
f1aeac7
Polish
OEvgeny Jun 13, 2024
a7c5d78
Remove Fluent Decorator and update test
OEvgeny Jun 13, 2024
500d0a9
Roll own Proxy implementation to bypass missing context
OEvgeny Jun 13, 2024
9d55558
Update test
OEvgeny Jun 13, 2024
e8e1552
Rework according to RCoR changes
OEvgeny Jun 14, 2024
20c39f8
Changelog
OEvgeny Jun 14, 2024
4c2b70d
Tweak changelog
OEvgeny Jun 14, 2024
ae7456c
Update RCoR
OEvgeny Jun 17, 2024
e863988
Sort
compulim Jun 17, 2024
4a878f0
Typo and sort
compulim Jun 17, 2024
4606eab
Better type checking
compulim Jun 17, 2024
076af8c
Sort
compulim Jun 18, 2024
cc75fe7
Sort
compulim Jun 18, 2024
b376200
Sort
compulim Jun 18, 2024
565f6ee
Newline
compulim Jun 18, 2024
fa1a8da
Newline
compulim Jun 18, 2024
f4cb3e3
Code styling
compulim Jun 18, 2024
30a04ec
Props optional/undefined
compulim Jun 18, 2024
49c1ddc
Code styling
compulim Jun 18, 2024
04b1ed5
Clean up
compulim Jun 18, 2024
756965d
Use EmptyObject for props instead of {} any
compulim Jun 18, 2024
dba3191
Sort and typo
compulim Jun 18, 2024
3ac54e6
Converge templateMiddleware and build user story
compulim Jun 18, 2024
1130f23
Fix no middleware and stabilize middleware prop
OEvgeny Jun 18, 2024
5e9eb3f
Refactor init
OEvgeny Jun 18, 2024
3bf9b41
Fix type
OEvgeny Jun 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/

## [Unreleased]

### Added

- (Experimental) Added initial decorators support, in PR [#5205](https://github.com/microsoft/BotFramework-WebChat/pull/5205), by [@OEvgeny](https://github.com/OEvgeny)
- Introduced internal `botframework-webchat-api/decorator` import, in PR [#5205](https://github.com/microsoft/BotFramework-WebChat/pull/5205), by [@OEvgeny](https://github.com/OEvgeny)
- Added `DecoratorComposer` and `ActivityDecorator` to be used for decorating activity border, in PR [#5205](https://github.com/microsoft/BotFramework-WebChat/pull/5205), by [@OEvgeny](https://github.com/OEvgeny)

### Fixed

- Improved performance for `useActivityWithRenderer`, in PR [#5172](https://github.com/microsoft/BotFramework-WebChat/pull/5172), by [@OEvgeny](https://github.com/OEvgeny)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 149 additions & 0 deletions __tests__/html/fluentTheme/withDecorator.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
<script crossorigin="anonymous" src="/__dist__/botframework-webchat-fluent-theme.production.min.js"></script>
<style>
.flair {
border-radius: inherit;
border: solid 2px red;
}

.loader {
border-bottom: solid 4px blue;
}
</style>
</head>
<body>
<main id="webchat"></main>
<script type="text/babel">
run(async function () {
const {
React,
ReactDOM: { render },
WebChat: {
decorator: { DecoratorComposer },
FluentThemeProvider,
ReactWebChat
}
} = window; // Imports in UMD fashion.

function Flair({ children }) {
return <div className="flair">{children}</div>;
}

function Loader({ children }) {
return <div className="loader">{children}</div>;
}

const decoratorMiddleware = [
init => init === 'activity border' && (next => request => (request.state === 'completion' ? Flair : next(request))),
init => init === 'activity border' && (next => request => (request.state === 'informative' ? Loader : next(request)))
];

const { directLine, store } = testHelpers.createDirectLineEmulator();

const App = () => <ReactWebChat
directLine={directLine}
store={store}
styleOptions={{
bubbleBorderRadius: 10,
typingAnimationBackgroundImage: `url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAUACgDASIAAhEBAxEB/8QAGgABAQACAwAAAAAAAAAAAAAAAAYCBwMFCP/EACsQAAECBQIEBQUAAAAAAAAAAAECAwAEBQYRBxITIjFBMlFhccFScoGh8f/EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwD0lctx023JVD9UeKOIcNoSNylkdcCMbauSmXHLOPUx8r4ZAcQtO1SM9Mj5iO1gtWo1syc7S2zMKYSptbIPNgnII8/5HBpRZ9RpaKjNVVCpUzLPAQ1nmA7qPl6fmAondRrcaqhkVTiiQrYXgglsH7vnpHc3DcNNoEimaqT4Q2s4bCRuUs+gEaLd05uNFVMmiS3o3YEwFDhlP1Z7e3WLzUuzahUKHRk0zM07TmeApvOFLGEjcM9+Xp6wFnbN0Uu5GnF0x4qW1je2tO1Sc9Djy9oRD6QWlU6PPzVSqjRlgtksttKPMcqBKiO3h/cIDacIQgEIQgEIQgP/2Q==')`
}}
/>;

render(
<FluentThemeProvider>
<DecoratorComposer middleware={decoratorMiddleware}>
<App />
</DecoratorComposer>
</FluentThemeProvider>,
document.getElementById('webchat')
);

await pageConditions.uiConnected();

await directLine.emulateIncomingActivity({
channelData: {
streamType: 'informative'
},
from: {
id: 'u-00001',
name: 'Bot',
role: 'bot'
},
id: 'm-00001',
text: 'Working on it...',
type: 'message'
});

await directLine.emulateIncomingActivity({
channelData: {
streamType: 'informative'
},
from: {
id: 'u-00001',
name: 'Bot',
role: 'bot'
},
id: 't-00001',
text: 'Working on it...',
type: 'typing'
});

await pageConditions.typingIndicatorShown();
await pageConditions.numActivitiesShown(1);
await host.snapshot();

const attachments = [
{
content: {
type: 'AdaptiveCard',
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
version: '1.5',
actions: [
{ type: 'Action.Submit', title: 'Button 1' },
{
type: 'Action.ShowCard',
title: 'Show card',
card: {
type: 'AdaptiveCard',
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
version: '1.5',
actions: [
{ type: 'Action.Submit', title: 'Button 2' },
{ type: 'Action.Submit', title: 'Button 3' }
]
}
}
]
},
contentType: 'application/vnd.microsoft.card.adaptive'
}
];
await directLine.emulateIncomingActivity({
id: 'm-00001',
from: {
id: 'u-00001',
name: 'Bot',
role: 'bot'
},
text: 'Work completed!',
channelData: { streamType: 'completion' },
attachments
});

await pageConditions.numActivitiesShown(1);

// THEN: Should render the activity.
await host.snapshot();
});
</script>
</body>
</html>
5 changes: 5 additions & 0 deletions __tests__/html/fluentTheme/withDecorator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */

describe('Fluent theme applied', () => {
test('with decorators', () => runHTML('fluentTheme/withDecorator'));
});
3 changes: 3 additions & 0 deletions packages/api/decorator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This is required for Webpack 4 which does not support named exports.
// eslint-disable-next-line no-undef
module.exports = require('./lib/decorator');
10 changes: 10 additions & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@
"types": "./lib/internal.d.ts",
"default": "./lib/internal.js"
}
},
"./decorator": {
compulim marked this conversation as resolved.
Show resolved Hide resolved
"import": {
"types": "./dist/botframework-webchat-api.decorator.d.mts",
"default": "./dist/botframework-webchat-api.decorator.mjs"
},
"require": {
"types": "./lib/decorator.d.ts",
"default": "./lib/decorator.js"
}
}
},
"publishConfig": {
Expand Down
4 changes: 4 additions & 0 deletions packages/api/src/decorator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { DecoratorComposer } from './private/DecoratorComposer';
export { default as ActivityDecorator } from './private/ActivityDecorator';
export { type DecoratorMiddleware } from './private/createDecoratorComposer';
export { default as ActivityDecoratorRequest } from './private/activityDecoratorRequest';
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import ActivityDecoratorRequest from './activityDecoratorRequest';
import templateMiddleware from './templateMiddleware';

const {
Provider: ActivityBorderDecoratorMiddlewareProvider,
Proxy: ActivityBorderDecoratorMiddlewareProxy,
initMiddleware: initActivityBorderDecoratorMiddleware,
types
} = templateMiddleware<{}, ActivityDecoratorRequest, typeof activityBorderDecoratorTypeName>(
'ActivityBorderDecoratorMiddleware'
);

type ActivityBorderDecoratorMiddleware = typeof types.middleware;
type ActivityBorderDecoratorMiddlewareProps = typeof types.props;
type ActivityBorderDecoratorMiddlewareRequest = typeof types.request;
type ActivityBorderDecoratorMiddlewareInit = typeof types.init;

const activityBorderDecoratorTypeName = 'activity border' as const;

export {
ActivityBorderDecoratorMiddlewareProvider,
ActivityBorderDecoratorMiddlewareProxy,
activityBorderDecoratorTypeName,
initActivityBorderDecoratorMiddleware,
type ActivityBorderDecoratorMiddleware,
type ActivityBorderDecoratorMiddlewareProps,
type ActivityBorderDecoratorMiddlewareRequest,
type ActivityBorderDecoratorMiddlewareInit
};
30 changes: 30 additions & 0 deletions packages/api/src/decorator/private/ActivityDecorator.tsx
OEvgeny marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { type ReactNode, memo, useMemo } from 'react';
import { ActivityBorderDecoratorMiddlewareProxy } from './ActivityBorderDecoratorMiddleware';
import { WebChatActivity } from 'botframework-webchat-core';
import { ActivityDecoratorRequest } from '..';

const ActivityDecoratorFallback = memo(({ children }) => <React.Fragment>{children}</React.Fragment>);

ActivityDecoratorFallback.displayName = 'ActivityDecoratorFallback';

const supportedActivityRoles = ['bot', 'chnnel', 'user', undefined] as const;
const supportedActivityStates = ['informative', 'completion', undefined] as const;

function ActivityDecorator({ children, activity }: Readonly<{ activity?: WebChatActivity; children?: ReactNode }>) {
const request = useMemo<ActivityDecoratorRequest>(
() => ({
from: supportedActivityRoles.includes(activity?.from?.role) ? activity?.from?.role : undefined,
state: supportedActivityStates.includes(activity?.channelData?.streamType)
? activity?.channelData?.streamType
: undefined
}),
[activity]
);
return (
<ActivityBorderDecoratorMiddlewareProxy fallbackComponent={ActivityDecoratorFallback} request={request}>
{children}
</ActivityBorderDecoratorMiddlewareProxy>
);
}

export default memo(ActivityDecorator);
6 changes: 6 additions & 0 deletions packages/api/src/decorator/private/DecoratorComposer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { memo } from 'react';
import createDecoratorComposer from './createDecoratorComposer';

export const DecoratorComposer = memo(createDecoratorComposer());

DecoratorComposer.displayName = 'DecoratorComposer';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type ActivityDecoratorRequestType = {
from: 'bot' | 'channel' | `user` | undefined;
state: 'completion' | 'informative' | undefined;
};

export default ActivityDecoratorRequestType;
36 changes: 36 additions & 0 deletions packages/api/src/decorator/private/createDecoratorComposer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { type ReactNode, useMemo } from 'react';
import {
ActivityBorderDecoratorMiddlewareProvider,
activityBorderDecoratorTypeName,
initActivityBorderDecoratorMiddleware,
type ActivityBorderDecoratorMiddleware
} from './ActivityBorderDecoratorMiddleware';

type DecoratorMiddlewareInit = typeof activityBorderDecoratorTypeName;

export type DecoratorComposerComponent = (
props: Readonly<{
children?: ReactNode;
middleware?: DecoratorMiddleware[];
}>
) => React.JSX.Element;

export type DecoratorMiddleware = (
init: DecoratorMiddlewareInit
) => ReturnType<ActivityBorderDecoratorMiddleware> | false;

const EMPTY_ARRAY = [];

export default (): DecoratorComposerComponent =>
({ children, middleware = EMPTY_ARRAY }) => {
const borderMiddlewares = useMemo(
() => initActivityBorderDecoratorMiddleware(middleware, activityBorderDecoratorTypeName),
[middleware]
);

return (
<ActivityBorderDecoratorMiddlewareProvider middleware={borderMiddlewares}>
{children}
</ActivityBorderDecoratorMiddlewareProvider>
);
};
62 changes: 62 additions & 0 deletions packages/api/src/decorator/private/templateMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { warnOnce } from 'botframework-webchat-core';
import { createChainOfResponsibility, type ComponentMiddleware } from 'react-chain-of-responsibility';
import { type EmptyObject } from 'type-fest';
import { any, array, custom, safeParse, type Output } from 'valibot';

export type MiddlewareWithInit<M extends ComponentMiddleware<unknown>, I = unknown> = (
init: I
) => ReturnType<M> | false;

export default function createMiddlewareFacility<
Props extends {} = EmptyObject,
Request extends {} = EmptyObject,
Init extends {} = undefined
>(name: string) {
type Middleware = ComponentMiddleware<Request, Props>;

const validateMiddleware = custom<Middleware>(input => typeof input === 'function', 'Middleware must be a function.');

const middlewareSchema = array(any([validateMiddleware]));

const isMiddleware = (middleware: unknown): middleware is Output<typeof middlewareSchema> =>
safeParse(middlewareSchema, middleware).success;

const warnInvalid = warnOnce(`"${name}" prop is invalid`);

const rectifyProps = (middleware: unknown): readonly Middleware[] => {
OEvgeny marked this conversation as resolved.
Show resolved Hide resolved
if (middleware) {
if (isMiddleware(middleware)) {
return Object.isFrozen(middleware) ? middleware : Object.freeze([...middleware]);
}

warnInvalid();
}

return Object.freeze([]);
};

const initMiddleware = (middleware: MiddlewareWithInit<Middleware>[], init: Init): readonly Middleware[] =>
rectifyProps(
middleware
.map(md => md(init))
.filter((enhancer): enhancer is ReturnType<Middleware> => !!enhancer)
.map(enhancer => () => enhancer)
);

const { Provider, Proxy } = createChainOfResponsibility<Request, Props>();

Provider.displayName = `${name}Provider`;
Proxy.displayName = `${name}Proxy`;

return {
types: {
middleware: undefined as Middleware,
props: undefined as Props,
request: undefined as Request,
init: undefined as Init
},
Provider,
Proxy,
initMiddleware
};
}
3 changes: 2 additions & 1 deletion packages/api/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export default defineConfig({
...baseConfig,
entry: {
'botframework-webchat-api': './src/index.ts',
'botframework-webchat-api.internal': './src/internal.ts'
'botframework-webchat-api.internal': './src/internal.ts',
'botframework-webchat-api.decorator': './src/decorator/index.ts'
},
noExternal: ['globalize']
});
2 changes: 2 additions & 0 deletions packages/bundle/src/index-minimal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// window['WebChat'] is required for TypeScript

import { StrictStyleOptions, StyleOptions } from 'botframework-webchat-api';
import * as decorator from 'botframework-webchat-api/decorator';
import { Constants, createStore, createStoreWithDevTools, createStoreWithOptions } from 'botframework-webchat-core';

import ReactWebChat, {
Expand Down Expand Up @@ -75,6 +76,7 @@ window['WebChat'] = {
createStore,
createStoreWithOptions,
createStyleSet,
decorator,
hooks,
ReactWebChat,
renderWebChat,
Expand Down
Loading
Loading