-
Notifications
You must be signed in to change notification settings - Fork 64
feat(cc-digital-channel): add-new-package #586
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
base: next
Are you sure you want to change the base?
Changes from all commits
43946ca
eb2e774
1bca291
7a694f1
efb887c
5ab4742
f8f12a7
f0432e0
598aa3f
6273e60
d3e037d
cd2bbd7
199a7e0
10fca07
97e4ac7
085cb53
eeb2ad1
402aea9
08cdf82
c1ea8d0
9b47030
94ca387
01e6c63
f052ab8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import globals from 'globals'; | ||
| import pluginJs from '@eslint/js'; | ||
| import tseslint from 'typescript-eslint'; | ||
| import pluginReact from 'eslint-plugin-react'; | ||
| import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; | ||
| import eslintConfigPrettier from 'eslint-config-prettier'; | ||
|
|
||
| export default [ | ||
| {files: ['**/src/**/*.{js,mjs,cjs,ts,jsx,tsx}']}, | ||
| {ignores: ['.babelrc.js', '*config.{js,ts}', 'dist', 'node_modules', 'coverage']}, | ||
| {languageOptions: {globals: globals.browser}}, | ||
| pluginJs.configs.recommended, | ||
| ...tseslint.configs.recommended, | ||
| { | ||
| ...pluginReact.configs.flat.recommended, | ||
| settings: {react: {version: 'detect'}}, | ||
| }, | ||
| eslintPluginPrettierRecommended, | ||
| eslintConfigPrettier, | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| const jestConfig = require('../../../jest.config.js'); | ||
|
|
||
| jestConfig.rootDir = '../../../'; | ||
| jestConfig.testMatch = ['**/cc-digital-channels/tests/**/*.ts', '**/cc-digital-channels/tests/**/*.tsx']; | ||
| jestConfig.transformIgnorePatterns = [ | ||
| '/node_modules/(?!(@momentum-design/components|@momentum-ui/web-components|@momentum-ui/react-collaboration|@lit|lit|cheerio|@popperjs|@webex-engage|@interactjs|react-error-boundary))', | ||
| ]; | ||
|
|
||
| module.exports = jestConfig; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| { | ||
| "name": "@webex/cc-digital-channels", | ||
| "description": "Webex Contact Center Widgets: Digital Channels", | ||
| "license": "Cisco's General Terms (https://www.cisco.com/site/us/en/about/legal/contract-experience/index.html)", | ||
| "version": "1.28.0-ccwidgets.126", | ||
| "main": "dist/index.js", | ||
| "types": "dist/types/index.d.ts", | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "files": [ | ||
| "dist/", | ||
| "package.json" | ||
| ], | ||
| "scripts": { | ||
| "clean": "rm -rf dist && rm -rf node_modules", | ||
| "clean:dist": "rm -rf dist", | ||
| "build": "yarn run -T tsc", | ||
| "build:src": "yarn run clean:dist && yarn run build && webpack", | ||
| "build:watch": "webpack --watch", | ||
| "test:unit": "jest --coverage", | ||
| "test:styles": "eslint", | ||
| "deploy:npm": "yarn npm publish" | ||
| }, | ||
| "dependencies": { | ||
| "@webex/cc-store": "workspace:*", | ||
| "cc-digital-interactions": "3.0.6-beta.1", | ||
| "mobx-react-lite": "^4.1.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@babel/core": "7.25.2", | ||
| "@babel/preset-env": "7.25.4", | ||
| "@babel/preset-react": "7.24.7", | ||
| "@babel/preset-typescript": "7.25.9", | ||
| "@eslint/js": "^9.20.0", | ||
| "@testing-library/dom": "10.4.0", | ||
| "@testing-library/jest-dom": "6.6.2", | ||
| "@testing-library/react": "16.0.1", | ||
| "@types/jest": "29.5.14", | ||
| "@webex/test-fixtures": "workspace:*", | ||
| "babel-jest": "29.7.0", | ||
| "babel-loader": "9.2.1", | ||
| "css-loader": "7.1.2", | ||
| "eslint": "^9.20.1", | ||
| "eslint-config-prettier": "^10.0.1", | ||
| "eslint-config-standard": "^17.1.0", | ||
| "eslint-plugin-import": "^2.25.2", | ||
| "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", | ||
| "eslint-plugin-prettier": "^5.2.3", | ||
| "eslint-plugin-promise": "^6.0.0", | ||
| "eslint-plugin-react": "^7.37.4", | ||
| "globals": "^16.0.0", | ||
| "jest": "29.7.0", | ||
| "jest-environment-jsdom": "29.7.0", | ||
| "prettier": "^3.5.1", | ||
| "sass": "1.79.5", | ||
| "sass-loader": "16.0.2", | ||
| "style-loader": "4.0.0", | ||
| "ts-jest": "^29.1.1", | ||
| "ts-loader": "9.5.1", | ||
| "typescript": "5.6.3", | ||
| "typescript-eslint": "^8.24.1", | ||
| "webpack": "5.94.0", | ||
| "webpack-cli": "5.1.4", | ||
| "webpack-merge": "6.0.1" | ||
| }, | ||
| "peerDependencies": { | ||
| "@momentum-ui/web-components": "^2.23.35", | ||
| "react": ">=18.3.1", | ||
| "react-dom": ">=18.3.1" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import React, {useMemo} from 'react'; | ||
| import Engage from 'cc-digital-interactions'; | ||
|
|
||
| import '@momentum-ui/web-components'; | ||
| import {DigitalChannelsComponentProps} from './digital-channels.types'; | ||
|
|
||
| /** | ||
| * Presentation component for Digital Channels. | ||
| * Renders the Engage widget with proper theming. | ||
| */ | ||
| const DigitalChannelsComponent: React.FunctionComponent<DigitalChannelsComponentProps> = ({ | ||
| conversationId, | ||
| jwtToken, | ||
| dataCenter, | ||
| currentTheme = 'LIGHT', | ||
| }) => { | ||
| // Create a stable key based on critical props to force remount when they change | ||
| // This prevents issues with the Froala editor trying to cleanup/reinitialize improperly | ||
| const componentKey = useMemo(() => { | ||
| return `${conversationId}-${jwtToken.slice(-8)}-${dataCenter}`; | ||
| }, [conversationId, jwtToken, dataCenter]); | ||
|
|
||
| const isDarkTheme = currentTheme === 'DARK'; | ||
|
|
||
| return ( | ||
| <div> | ||
| <md-theme id="app-theme" theme="momentumV2" {...(isDarkTheme ? {darktheme: true} : {lighttheme: true})}> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did we confirm if this is required? To use widgets we have asked dev to wrap the app in Theme and Icon Provider, so if a developer does that and here we are again wrapping the component that mean the Engage widget is wrapped twice. Just wanna ensure this doesnt break anything. Maybe its worthwhile to check with momentum team about this, We are wrapping a web-component in a react component. |
||
| <Engage | ||
| key={componentKey} | ||
| conversationId={conversationId} | ||
| jwtToken={jwtToken} | ||
| dataCenter={dataCenter} | ||
| interactionId="" | ||
| readonly={false} | ||
| theme={isDarkTheme ? 'dark' : 'light'} | ||
| isVisualRebrand={true} | ||
| /> | ||
| </md-theme> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export {DigitalChannelsComponent}; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||
| import {ITask} from '@webex/cc-store'; | ||||||
|
|
||||||
| export interface UseDigitalChannelsInitProps { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Maybe we can rename this and remove the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, Im assuming this is for hook. We were using this naming convention in web-client, in this repo we just use the work hook
Suggested change
|
||||||
| currentTask: ITask; | ||||||
| jwtToken: string; | ||||||
| dataCenter: string; | ||||||
| logger: { | ||||||
| log: (message: string, meta?: Record<string, unknown>) => void; | ||||||
| error: (message: string, error?: unknown, meta?: Record<string, unknown>) => void; | ||||||
| }; | ||||||
| isDigitalChannelsInitialized: boolean; | ||||||
| setDigitalChannelsInitialized: (value: boolean) => void; | ||||||
| skipInit?: boolean; | ||||||
| } | ||||||
|
|
||||||
| export interface UseDigitalChannelsDataProps { | ||||||
| getAccessToken: () => Promise<string>; | ||||||
| currentTask: ITask | null; | ||||||
| logger?: { | ||||||
| log: (message: string, meta?: Record<string, unknown>) => void; | ||||||
| error: (message: string, meta?: Record<string, unknown>) => void; | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| export interface DigitalChannelsProps { | ||||||
| currentTheme?: string; | ||||||
| } | ||||||
|
|
||||||
| export interface DigitalChannelsComponentProps { | ||||||
| conversationId: string; | ||||||
| jwtToken: string; | ||||||
| dataCenter: string; | ||||||
| currentTheme?: string; | ||||||
| } | ||||||
Shreyas281299 marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import React from 'react'; | ||
| import {observer} from 'mobx-react-lite'; | ||
| import {ErrorBoundary} from 'react-error-boundary'; | ||
|
|
||
| import store from '@webex/cc-store'; | ||
| import {useDigitalChannelsInit, useDigitalChannelsData} from '../helper'; | ||
| import {DigitalChannelsComponent} from './DigitalChannelsComponent'; | ||
| import {DigitalChannelsProps} from './digital-channels.types'; | ||
|
|
||
| const DigitalChannelsInternal: React.FunctionComponent<DigitalChannelsProps> = observer(({currentTheme}) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to move this to another file just for organisation perspective perhaps. |
||
| const {logger, currentTask, isDigitalChannelsInitialized, setDigitalChannelsInitialized, getAccessToken, dataCenter} = | ||
| store; | ||
|
|
||
| // Fetch JWT token and conversation ID | ||
| const {jwtToken, conversationId, hasError} = useDigitalChannelsData({ | ||
| getAccessToken, | ||
| currentTask, | ||
| logger, | ||
| }); | ||
|
|
||
| // Initialize Digital Channels app once we have all required data | ||
| const {initialized} = useDigitalChannelsInit({ | ||
| currentTask: currentTask || ({} as typeof currentTask), | ||
| jwtToken: jwtToken || '', | ||
| dataCenter: dataCenter || '', | ||
| logger, | ||
| isDigitalChannelsInitialized, | ||
| setDigitalChannelsInitialized, | ||
| // Skip initialization if we don't have required data | ||
| skipInit: !currentTask || !jwtToken || !dataCenter, | ||
| }); | ||
|
|
||
| // Early return after all hooks are called | ||
| if (!currentTask || !jwtToken || !dataCenter || hasError || !initialized || !conversationId) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <DigitalChannelsComponent | ||
| conversationId={conversationId} | ||
| jwtToken={jwtToken} | ||
| dataCenter={dataCenter} | ||
| currentTheme={currentTheme} | ||
| /> | ||
| ); | ||
| }); | ||
|
|
||
| const DigitalChannels: React.FunctionComponent<DigitalChannelsProps> = (props) => { | ||
| return ( | ||
| <ErrorBoundary | ||
| fallbackRender={() => <></>} | ||
| onError={(error: Error) => { | ||
| if (store.onErrorCallback) store.onErrorCallback('DigitalChannels', error); | ||
| }} | ||
| > | ||
| <DigitalChannelsInternal {...props} /> | ||
| </ErrorBoundary> | ||
| ); | ||
| }; | ||
|
|
||
| export {DigitalChannels}; | ||
Shreyas281299 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| import {useEffect, useState, useMemo} from 'react'; | ||
| import {initializeApp} from 'cc-digital-interactions'; | ||
|
|
||
| import {UseDigitalChannelsInitProps, UseDigitalChannelsDataProps} from './digital-channels/digital-channels.types'; | ||
|
|
||
| /** | ||
| * Hook to handle Digital Channels initialization. | ||
| * Ensures initialization happens only once per session using store flag. | ||
| */ | ||
| export const useDigitalChannelsInit = (props: UseDigitalChannelsInitProps) => { | ||
| const { | ||
| currentTask, | ||
| jwtToken, | ||
| dataCenter, | ||
| logger, | ||
| isDigitalChannelsInitialized, | ||
| setDigitalChannelsInitialized, | ||
| skipInit = false, | ||
| } = props; | ||
|
|
||
| const [initialized, setInitialized] = useState(isDigitalChannelsInitialized); | ||
|
|
||
| useEffect(() => { | ||
| // Skip initialization if required data is not available | ||
| if (skipInit) { | ||
| return; | ||
| } | ||
|
|
||
| const initialize = async () => { | ||
| // Initialize the digital channels app only once per session | ||
| if (!isDigitalChannelsInitialized) { | ||
| logger.log( | ||
| `[DIGITAL_CHANNELS_INIT] 🚀 Starting Digital Channels initialization for the FIRST TIME (dataCenter: ${dataCenter})...`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we not have emogis in logs. |
||
| { | ||
| module: 'cc-digital-channels', | ||
| method: 'useDigitalChannelsInit', | ||
| } | ||
| ); | ||
|
|
||
| try { | ||
| await initializeApp(dataCenter, jwtToken); | ||
| setDigitalChannelsInitialized(true); | ||
| setInitialized(true); | ||
| logger.log('[DIGITAL_CHANNELS_INIT] ✅ Digital Channels app initialized SUCCESSFULLY', { | ||
| module: 'cc-digital-channels', | ||
| method: 'useDigitalChannelsInit', | ||
| }); | ||
| } catch (error) { | ||
| const errorMessage = error instanceof Error ? error.message : 'Unknown error'; | ||
| logger.error(`[DIGITAL_CHANNELS_INIT] ❌ Failed to initialize Digital Channels app: ${errorMessage}`, { | ||
| module: 'cc-digital-channels', | ||
| method: 'useDigitalChannelsInit', | ||
| error, | ||
| }); | ||
| } | ||
| } else { | ||
| logger.log('[DIGITAL_CHANNELS_INIT] ✅ App already initialized. Skipping re-initialization.', { | ||
| module: 'cc-digital-channels', | ||
| method: 'useDigitalChannelsInit', | ||
| }); | ||
| setInitialized(true); | ||
| } | ||
| }; | ||
|
|
||
| initialize(); | ||
| }, [currentTask, skipInit, jwtToken]); | ||
|
|
||
| return {initialized}; | ||
| }; | ||
|
|
||
| /** | ||
| * Hook to handle fetching Digital Channels data (token and conversationId). | ||
| * Centralizes token fetching logic to keep the component clean. | ||
| */ | ||
| export const useDigitalChannelsData = (props: UseDigitalChannelsDataProps) => { | ||
| const {getAccessToken, currentTask, logger} = props; | ||
|
|
||
| const [jwtToken, setJwtToken] = useState<string>(''); | ||
| const [tokenError, setTokenError] = useState<boolean>(false); | ||
|
|
||
| // Fetch access token from the store | ||
| useEffect(() => { | ||
| const fetchToken = async () => { | ||
| try { | ||
| const token = await getAccessToken(); | ||
| setJwtToken(token); | ||
| } catch (error) { | ||
| logger?.error('[DIGITAL_CHANNELS] ❌ Failed to get access token', { | ||
| module: 'cc-digital-channels', | ||
| method: 'useDigitalChannelsData.fetchToken', | ||
| error, | ||
| }); | ||
| setTokenError(true); | ||
| } | ||
| }; | ||
| fetchToken(); | ||
| }, [getAccessToken, logger]); | ||
|
|
||
| // Extract conversationId from currentTask (always call this, return empty string if no task) | ||
| const conversationId = useMemo(() => { | ||
| if (!currentTask) return ''; | ||
| return ( | ||
| (currentTask.data.interaction as {callAssociatedDetails?: {mediaResourceId?: string}}).callAssociatedDetails | ||
| ?.mediaResourceId || '' | ||
| ); | ||
| }, [currentTask]); | ||
|
|
||
| const hasError = tokenError; | ||
|
|
||
| return { | ||
| jwtToken, | ||
| conversationId, | ||
| tokenError, | ||
| hasError, | ||
| }; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import {DigitalChannels} from './digital-channels'; | ||
|
|
||
| export {DigitalChannels}; | ||
| export default DigitalChannels; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // Declare custom HTML elements used by the Webex Engage components | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we wont require md-theme, we can get rid of this file as well
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if this comment is still applicable... but if so, we can remove this file.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets check this please. |
||
| declare global { | ||
| namespace JSX { | ||
| interface IntrinsicElements { | ||
| 'md-theme': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & { | ||
| theme?: string; | ||
| class?: string; | ||
| darktheme?: boolean; | ||
| lighttheme?: boolean; | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export {}; | ||
Uh oh!
There was an error while loading. Please reload this page.