diff --git a/README.md b/README.md index 325a802..a2eaea7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,50 @@ -# flex-plugin-ts -A base TypeScript template for Flex Plugins +# Your custom Twilio Flex Plugin + +Twilio Flex Plugins allow you to customize the appearance and behavior of [Twilio Flex](https://www.twilio.com/flex). If you want to learn more about the capabilities and how to use the API, check out our [Flex documentation](https://www.twilio.com/docs/flex). + +## Setup + +Make sure you have [Node.js](https://nodejs.org) as well as [`npm`](https://npmjs.com) installed. + +Afterwards, install the dependencies by running `npm install`: + +```bash +cd {{pluginFileName}} + +# If you use npm +npm install +``` + +## Development + +In order to develop locally, you can use the Webpack Dev Server by running: + +```bash +npm start +``` + +This will automatically start up the Webpack Dev Server and open the browser for you. Your app will run on `http://localhost:3000`. If you want to change that you can do this by setting the `PORT` environment variable: + +```bash +PORT=3001 npm start +``` + +When you make changes to your code, the browser window will be automatically refreshed. + +## Deploy + +When you are ready to deploy your plugin, in your terminal run: + +```bash +npm run deploy +``` + +This will publish your plugin as a Private Asset that is accessible by the Functions & Assets API. If you want to deploy your plugin as a Public Asset, you may pass --public to your deploy command: + +```bash +npm run deploy --public +``` + +For more details on deploying your plugin, refer to the [deploying your plugin guide](https://www.twilio.com/docs/flex/plugins#deploying-your-plugin). + +Note: Common packages like `React`, `ReactDOM`, `Redux` and `ReactRedux` are not bundled with the build because they are treated as external dependencies so the plugin will depend on Flex to provide them globally. \ No newline at end of file diff --git a/craco.config.js b/craco.config.js new file mode 100644 index 0000000..aea5856 --- /dev/null +++ b/craco.config.js @@ -0,0 +1,12 @@ +const config = require('craco-config-flex-plugin'); + +module.exports = { + ...config, + plugins: [ + // Customize app configuration (such as webpack, devServer, linter, etc) by creating a Craco plugin. + // See https://github.com/sharegate/craco/tree/master/packages/craco#develop-a-plugin for more detail. + // + // Please note that Craco plugins have nothing to do with Flex plugins; it's just a naming coincidence. + // Changes to this file are optional, you will not need to modify it for normal Flex Plugin development. + ] +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..6dd96d2 --- /dev/null +++ b/package.json @@ -0,0 +1,56 @@ +{ + "name": "{{name}}", + "version": "0.0.0", + "private": true, + "scripts": { + "bootstrap": "flex-plugin check-start", + "prebuild": "rimraf build && npm run bootstrap", + "build": "flex-plugin build", + "clear": "flex-plugin clear", + "predeploy": "npm run build", + "deploy": "flex-plugin deploy", + "eject": "flex-plugin eject", + "info": "flex-plugin info", + "postinstall": "npm run bootstrap", + "list": "flex-plugin list", + "remove": "flex-plugin remove", + "prestart": "npm run bootstrap", + "start": "flex-plugin start", + "test": "flex-plugin test --env=jsdom" + }, + "dependencies": { + "craco-config-flex-plugin": "^3", + "flex-plugin": "^3", + "flex-plugin-scripts": "^3", + "react": "16.5.2", + "react-dom": "16.5.2", + "react-emotion": "9.2.6", + "react-scripts": "3.4.1", + "typescript": "^3.6.4" + }, + "devDependencies": { + "@twilio/flex-ui": "^1", + "@types/enzyme": "^3.10.3", + "@types/jest": "^24.0.18", + "@types/node": "^12.7.12", + "@types/react": "^16.8.16", + "@types/react-dom": "^16.8.4", + "@types/react-redux": "^7.1.1", + "babel-polyfill": "^6.26.0", + "enzyme": "^3.10.0", + "enzyme-adapter-react-16": "^1.14.0", + "rimraf": "^3.0.0" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/public/appConfig.example.js b/public/appConfig.example.js new file mode 100644 index 0000000..e497f9e --- /dev/null +++ b/public/appConfig.example.js @@ -0,0 +1,20 @@ +// your account sid +var accountSid = 'accountSid'; + +// set to /plugins.json for local dev +// set to /plugins.local.build.json for testing your build +// set to "" for the default live plugin loader +var pluginServiceUrl = '/plugins.json'; + +var appConfig = { + pluginService: { + enabled: true, + url: pluginServiceUrl, + }, + sso: { + accountSid: accountSid + }, + ytica: false, + logLevel: 'debug', + showSupervisorDesktopView: true, +}; diff --git a/public/appConfig.js b/public/appConfig.js new file mode 100644 index 0000000..e2e5ed3 --- /dev/null +++ b/public/appConfig.js @@ -0,0 +1,18 @@ +// your account sid +var accountSid = '{{accountSid}}'; + +// set to /plugins.json for local dev +// set to /plugins.local.build.json for testing your build +// set to "" for the default live plugin loader +var pluginServiceUrl = '/plugins.json'; + +var appConfig = { + pluginService: { + enabled: true, + url: pluginServiceUrl, + }, + sso: { + accountSid: accountSid + }, + logLevel: 'debug', +}; diff --git a/public/plugins.json b/public/plugins.json new file mode 100644 index 0000000..fa8b43b --- /dev/null +++ b/public/plugins.json @@ -0,0 +1 @@ +{{pluginJsonContent}} diff --git a/public/plugins.local.build.json b/public/plugins.local.build.json new file mode 100644 index 0000000..d1d5349 --- /dev/null +++ b/public/plugins.local.build.json @@ -0,0 +1,13 @@ +[ + { + "name": "{{pluginClassName}}", + "version": "0.0.0", + "class": "{{pluginClassName}}", + "requires": [ + { + "@twilio/flex-ui": "{{flexSdkVersion}}" + } + ], + "src": "http://127.0.0.1:8085" + } +] diff --git a/src/DemoPlugin.tsx b/src/DemoPlugin.tsx new file mode 100644 index 0000000..32e675d --- /dev/null +++ b/src/DemoPlugin.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import * as Flex from '@twilio/flex-ui'; +import { FlexPlugin } from 'flex-plugin'; + +import CustomTaskListContainer from './components/CustomTaskList/CustomTaskList.Container'; +import reducers, { namespace } from './states'; + +const PLUGIN_NAME = '{{pluginClassName}}'; + +export default class {{pluginClassName}} extends FlexPlugin { + constructor() { + super(PLUGIN_NAME); + } + + /** + * This code is run when your plugin is being started + * Use this to modify any UI components or attach to the actions framework + * + * @param flex { typeof Flex } + * @param manager { Flex.Manager } + */ + init(flex: typeof Flex, manager: Flex.Manager) { + this.registerReducers(manager); + + const options: Flex.ContentFragmentProps = { sortOrder: -1 }; + flex.AgentDesktopView + .Panel1 + .Content + .add(, options); + } + + /** + * Registers the plugin reducers + * + * @param manager { Flex.Manager } + */ + private registerReducers(manager: Flex.Manager) { + if (!manager.store.addReducer) { + // tslint: disable-next-line + console.error(`You need FlexUI > 1.9.0 to use built-in redux; you are currently on ${Flex.VERSION}`); + return; + } + + manager.store.addReducer(namespace, reducers); + } +} diff --git a/src/components/CustomTaskList/CustomTaskList.Container.ts b/src/components/CustomTaskList/CustomTaskList.Container.ts new file mode 100644 index 0000000..3258752 --- /dev/null +++ b/src/components/CustomTaskList/CustomTaskList.Container.ts @@ -0,0 +1,24 @@ +import { AppState } from '../../states'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; + +import { Actions } from '../../states/CustomTaskListState'; +import CustomTaskList from './CustomTaskList'; + +export interface StateToProps { + isOpen: boolean; +} + +export interface DispatchToProps { + dismissBar: () => void; +} + +const mapStateToProps = (state: AppState): StateToProps => ({ + isOpen: state['{{pluginNamespace}}'].customTaskList.isOpen, +}); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchToProps => ({ + dismissBar: bindActionCreators(Actions.dismissBar, dispatch), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(CustomTaskList); diff --git a/src/components/CustomTaskList/CustomTaskList.Styles.ts b/src/components/CustomTaskList/CustomTaskList.Styles.ts new file mode 100644 index 0000000..ec1077f --- /dev/null +++ b/src/components/CustomTaskList/CustomTaskList.Styles.ts @@ -0,0 +1,14 @@ +import { default as styled } from 'react-emotion'; + +export const CustomTaskListComponentStyles = styled('div')` + padding: 10px; + margin: 0px; + color: #fff; + background: #000; + + .accented { + color: red; + cursor: pointer; + float: right; + } +`; diff --git a/src/components/CustomTaskList/CustomTaskList.tsx b/src/components/CustomTaskList/CustomTaskList.tsx new file mode 100644 index 0000000..6f7dcae --- /dev/null +++ b/src/components/CustomTaskList/CustomTaskList.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import { CustomTaskListComponentStyles } from './CustomTaskList.Styles'; +import { StateToProps, DispatchToProps } from './CustomTaskList.Container'; + +interface OwnProps { + // Props passed directly to the component +} + +// Props should be a combination of StateToProps, DispatchToProps, and OwnProps +type Props = StateToProps & DispatchToProps & OwnProps; + +// It is recommended to keep components stateless and use redux for managing states +const CustomTaskList = (props: Props) => { + if (!props.isOpen) { + return null; + } + + return ( + + This is a dismissible demo component + + close + + + ); +}; + +export default CustomTaskList; diff --git a/src/components/__tests__/CustomTaskListComponent.spec.tsx b/src/components/__tests__/CustomTaskListComponent.spec.tsx new file mode 100644 index 0000000..5c7e02c --- /dev/null +++ b/src/components/__tests__/CustomTaskListComponent.spec.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import CustomTaskList from '../CustomTaskList/CustomTaskList'; + +describe('CustomTaskListComponent', () => { + it('should render demo component', () => { + const props = { + isOpen: true, + dismissBar: () => undefined, + }; + const wrapper = shallow(); + expect(wrapper.render().text()).toMatch('This is a dismissible demo component'); + }); +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..3ccdba7 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,4 @@ +import * as FlexPlugin from 'flex-plugin'; +import {{pluginClassName}} from './{{pluginClassName}}'; + +FlexPlugin.loadPlugin({{pluginClassName}}); diff --git a/src/setupTests.js b/src/setupTests.js new file mode 100644 index 0000000..9554ff9 --- /dev/null +++ b/src/setupTests.js @@ -0,0 +1,8 @@ +require('babel-polyfill'); + +import { configure } from 'enzyme/build'; +import Adapter from 'enzyme-adapter-react-16/build'; + +configure({ + adapter: new Adapter(), +}); diff --git a/src/states/CustomTaskListState.ts b/src/states/CustomTaskListState.ts new file mode 100644 index 0000000..1524caa --- /dev/null +++ b/src/states/CustomTaskListState.ts @@ -0,0 +1,29 @@ +import { Action } from './index'; + +const ACTION_DISMISS_BAR = 'DISMISS_BAR'; + +export interface CustomTaskListState { + isOpen: boolean; +} + +const initialState: CustomTaskListState = { + isOpen: true, +}; + +export class Actions { + public static dismissBar = (): Action => ({ type: ACTION_DISMISS_BAR }); +} + +export function reduce(state: CustomTaskListState = initialState, action: Action) { + switch (action.type) { + case ACTION_DISMISS_BAR: { + return { + ...state, + isOpen: false, + }; + } + + default: + return state; + } +} diff --git a/src/states/index.ts b/src/states/index.ts new file mode 100644 index 0000000..75a19a6 --- /dev/null +++ b/src/states/index.ts @@ -0,0 +1,26 @@ +import { AppState as FlexAppState } from '@twilio/flex-ui'; +import { combineReducers, Action as ReduxAction } from 'redux'; + +import { CustomTaskListState, reduce as CustomTaskListReducer } from './CustomTaskListState'; + +// Register your redux store under a unique namespace +export const namespace = '{{pluginNamespace}}'; + +// Extend this payload to be of type that your ReduxAction is +export interface Action extends ReduxAction { + payload?: any; +} + +// Register all component states under the namespace +export interface AppState { + flex: FlexAppState; + '{{pluginNamespace}}': { + customTaskList: CustomTaskListState; + // Other states + }; +} + +// Combine the reducers +export default combineReducers({ + customTaskList: CustomTaskListReducer +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9f3b58a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "rootDir": ".", + "outDir": "build", + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "include": [ + "./src/**/*" + ], + "exclude": [ + "./**/*.test.ts", + "./**/*.test.tsx", + "./**/__mocks__/*.ts", + "./**/__mocks__/*.tsx" + ] +}