Compare commits

...
Author SHA1 Message Date
Shivam Mishra 1b31b8b93c fix: spacing 2025-06-17 13:22:59 +05:30
Shivam Mishra 4502ed6213 feat: allow disabling private note 2025-06-17 13:17:06 +05:30
Shivam Mishra f0e376c0d9 feat: better versioning 2025-06-12 14:43:11 +05:30
Shivam Mishra 0be8af9c3d feat: disable context menu 2025-06-12 14:37:48 +05:30
Shivam Mishra f6c4224400 docs: development instructions 2025-06-12 14:36:12 +05:30
6 changed files with 137 additions and 12 deletions
@@ -315,6 +315,9 @@ const componentToRender = computed(() => {
});
const shouldShowContextMenu = computed(() => {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ISOLATED_SHELL__) return false;
return !props.contentAttributes?.isUnsupported;
});
@@ -0,0 +1,95 @@
# Chatwoot React Components
This module exports Chatwoot UI components as React components that can be embedded in other applications. It uses a hybrid approach combining Vue Web Components with React wrappers.
## Architecture
The system works by:
1. **Vue Components** → Compiled as custom web components using Vue's `customElement: true`
2. **React Wrappers** → Provide React-friendly interfaces to the Vue web components
3. **Vite Build** → Packages everything into distributable ES/CJS modules
## Build System
### Build Modes (vite.config.ts)
- **`BUILD_MODE=react-components`** - Builds React components library
- Entry point: `./app/javascript/react-components/src/index.jsx`
- Output: ES modules (`react-components.es.js`) and CommonJS (`react-components.cjs.js`)
- External dependencies: `react` and `react-dom` (peer dependencies)
### Available Scripts (package.json)
```bash
# Build the React components once
pnpm build:react
# Build and watch for changes during development
pnpm dev:react
# Package for NPM distribution (builds + creates dist/react-components/)
pnpm package:react
```
## Usage
In any React app, version 16+, you could use the following snippet
```jsx
import * as ChatwootComponents from '@chatwoot/agent-react-components';
import '@chatwoot/agent-react-components/style.css'
const { ChatwootProvider, ChatwootConversation } = ChatwootComponents;
function App() {
return (
<ChatwootProvider
baseURL="http://localhost:3000"
accountId="1"
conversationId={13918}
userToken="XLwgvCQiewUJMGuYpZqa4J3M"
pubsubToken="HkAhoq8aWm2ecdY2tn9FV4BU"
websocketURL="ws://localhost:3000"
>
<ChatwootConversation/>
</ChatwootProvider>
)
}
export default App
```
The `<ChatwootProvider>` component ensures all the data necessary for mounting the web component is present before rendering the ChatwootConversation.
The package also exposes a `useChatwoot` hook for accessing the config instance being used by the conversation, it can be used as a read-only source of truth for the config being used.
## Development Workflow
To test this package, you can create a separate React application where you can use the snippet from above, run the following command in your Rails App:
```bash
pnpm dev:react
```
This will start a development server that serves the React components and their dependencies.
Following this, in a separate terminal, run the following command
```bash
watchexec --restart -N --watch public/packs/react-components.es.js "pnpm package:react --copyMode"
```
This will ensure that the built files are copied correctly to the dist/react-components folder.
The `package:react` script has a copy mode, which just copies the build from the `public/` folder to the dist folder for linking purposes. It also creates a package.json file with proper entry points, ensure you run the script without copyMode once before.
Once done, in your React app, you can use `pnpm link <rails-app-dir>/dist/react-components` to link the package. Running the development server will automatically link the package and any changes to it.
## Publishing
The `scripts/package-react-components.js` handles:
- Building the components with Vite
- Copying built files to `dist/react-components/`
- Generating `package.json` with proper entry points
- Creating development versions with git hash suffixes
Once the script is packaged, you can run `npm publish` to publish the package. It will be published as `@chatwoot/agent-react-components` with a version that is the latest commit hash
@@ -18,6 +18,7 @@ export const ChatwootProvider = ({
conversationId,
disableUpload,
disableEditor,
disablePrivateNote,
children,
}) => {
const isInitialized = useRef(false);
@@ -38,6 +39,7 @@ export const ChatwootProvider = ({
accountId: Number(accountId),
conversationId: Number(conversationId),
disableEditor: disableEditor || false,
disablePrivateNote: disablePrivateNote || false,
disableUpload: disableUpload || false,
websocketURL: websocketURL,
pubsubToken: pubsubToken,
@@ -56,6 +58,7 @@ export const ChatwootProvider = ({
window.__PUBSUB_TOKEN__ = config.pubsubToken;
window.__WOOT_CONVERSATION_ID__ = config.conversationId;
window.__EDITOR_DISABLE_UPLOAD__ = config.disableUpload;
window.__EDITOR_DISABLE_PRIVATE_NOTE__ = config.disablePrivateNote;
window.__DISABLE_EDITOR__ = config.disableEditor;
window.__WOOT_ISOLATED_SHELL__ = true;
/* eslint-enable no-underscore-dangle */
+9 -1
View File
@@ -107,6 +107,10 @@ export default {
maxLength() {
return MESSAGE_MAX_LENGTH.GENERAL;
},
allowPrivateNote() {
// eslint-disable-next-line no-underscore-dangle
return window.__EDITOR_DISABLE_PRIVATE_NOTE__ !== true;
},
isEditorDisabled() {
// eslint-disable-next-line no-underscore-dangle
return window.__DISABLE_EDITOR__;
@@ -123,7 +127,7 @@ export default {
},
replyBoxClass() {
return {
'is-private': this.isPrivate,
'is-private': this.isPrivate && this.allowPrivateNote,
'is-focused': this.isFocused || this.hasAttachments,
'pointer-events-none grayscale opacity-70': this.isEditorDisabled,
};
@@ -132,6 +136,7 @@ export default {
return this.attachedFiles.length;
},
isOnPrivateNote() {
if (!this.allowPrivateNote) return false;
return this.replyType === REPLY_EDITOR_MODES.NOTE;
},
isOnExpandedLayout() {
@@ -222,6 +227,8 @@ export default {
}
},
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
if (!this.allowPrivateNote) return;
const { can_reply: canReply } = this.currentChat;
this.$store.dispatch('draftMessages/setReplyEditorMode', {
mode,
@@ -381,6 +388,7 @@ export default {
<template>
<div ref="replyEditor" class="reply-box" :class="replyBoxClass">
<ReplyTopPanel
v-if="allowPrivateNote"
:mode="replyType"
disable-popout
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
+10 -1
View File
@@ -53,6 +53,11 @@ const allMessages = computed(() => {
return useCamelCase(conversation.value.messages, { deep: true }).reverse();
});
const disablePrivateNote = computed(() => {
// eslint-disable-next-line no-underscore-dangle
return window.__EDITOR_DISABLE_PRIVATE_NOTE__ === true;
});
const fetchMore = async () => {
if (isFetching.value) return;
if (!conversation?.value?.id) return;
@@ -99,7 +104,11 @@ useInfiniteScroll(messageListRef, useThrottleFn(fetchMore, 1000), {
<div class="relative">
<ul
ref="messageListRef"
class="px-4 pt-4 flex flex-col-reverse pb-60 bg-n-background overflow-scroll h-screen"
class="px-4 pt-4 flex flex-col-reverse bg-n-background overflow-scroll h-screen"
:class="{
'pb-60': !disablePrivateNote,
'pb-48': disablePrivateNote,
}"
>
<div
v-if="isAnyoneTyping"
+17 -10
View File
@@ -35,27 +35,34 @@ function generatePackageJson(packageDir) {
// Read version from main package.json
const mainPackage = JSON.parse(fs.readFileSync('package.json', 'utf8'));
// Get current git commit hash for development builds
function getVersionSuffix() {
// Generate a clean epoch-based version
function generateTimestampVersion() {
try {
const epochSeconds = Math.floor(Date.now() / 1000);
// Get git hash for reference
const gitHash = execSync('git rev-parse --short HEAD', {
encoding: 'utf8',
stdio: 'pipe',
}).trim();
return `dev.${gitHash}`;
// Parse base version and increment patch
const [major, minor, patch] = mainPackage.version.split('.').map(Number);
const newPatch = patch + 1;
return `${major}.${minor}.${newPatch}-${epochSeconds}.${gitHash}`;
} catch (error) {
console.warn(' ⚠️ Could not get git hash, using beta.1');
return 'beta.1';
console.warn(
' ⚠️ Could not generate timestamp version, using incremented patch'
);
const [major, minor, patch] = mainPackage.version.split('.').map(Number);
return `${major}.${minor}.${patch + 1}`;
}
}
const versionSuffix = getVersionSuffix();
const finalVersion = `${mainPackage.version}-${versionSuffix}`;
const finalVersion = generateTimestampVersion();
console.log(
` 📋 Package version: ${finalVersion} (suffix: ${versionSuffix})`
);
console.log(` 📋 Package version: ${finalVersion}`);
const packageJson = {
name: '@chatwoot/agent-react-components',