Compare commits

..
Author SHA1 Message Date
google-labs-jules[bot] 99ef76562b Allow agents to export contacts
This change modifies the ContactPolicy to allow users with the agent role to export contacts. Previously, only administrators could perform this action.

Includes tests to verify that both agents and administrators can export contacts.
2025-06-09 16:47:58 +00:00
google-labs-jules[bot] 3b29214dfc Allow agents to export contacts
This change modifies the ContactPolicy to allow users with the agent role to export contacts. Previously, only administrators could perform this action.

Includes tests to verify that both agents and administrators can export contacts.
2025-06-09 16:40:30 +00:00
44 changed files with 85 additions and 2151 deletions
-3
View File
@@ -94,6 +94,3 @@ yarn-debug.log*
.vscode
.claude/settings.local.json
.cursor
# react component
dist
@@ -15,12 +15,6 @@ class ApiClient {
// eslint-disable-next-line class-methods-use-this
get accountIdFromRoute() {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ACCOUNT_ID__) {
// eslint-disable-next-line no-underscore-dangle
return window.__WOOT_ACCOUNT_ID__;
}
const isInsideAccountScopedURLs =
window.location.pathname.includes('/app/accounts');
+1 -12
View File
@@ -8,14 +8,7 @@ import {
} from '../store/utils/api';
export default {
async validityCheck() {
if (this.hasAuthToken()) {
const urlData = endPoints('profileUpdate');
const response = await axios.get(urlData.url);
// to match the response signature of the validityCheck endpoint
return Promise.resolve({ data: { payload: response } });
}
validityCheck() {
const urlData = endPoints('validityCheck');
return axios.get(urlData.url);
},
@@ -38,10 +31,6 @@ export default {
hasAuthCookie() {
return !!Cookies.get('cw_d_session_info');
},
hasAuthToken() {
// eslint-disable-next-line no-underscore-dangle
return !!window.__WOOT_ACCESS_TOKEN__;
},
getAuthData() {
if (this.hasAuthCookie()) {
const savedAuthInfo = Cookies.get('cw_d_session_info');
+1 -8
View File
@@ -1,14 +1,7 @@
/* global axios */
import CacheEnabledApiClient from './CacheEnabledApiClient';
// import ApiClient from './ApiClient';
import ApiClient from './ApiClient';
// eslint-disable-next-line no-underscore-dangle
const BaseClass = window.__WOOT_ISOLATED_SHELL__
? ApiClient
: CacheEnabledApiClient;
class Inboxes extends BaseClass {
class Inboxes extends CacheEnabledApiClient {
constructor() {
super('inboxes', { accountScoped: true });
}
@@ -315,9 +315,6 @@ const componentToRender = computed(() => {
});
const shouldShowContextMenu = computed(() => {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ISOLATED_SHELL__) return false;
return !props.contentAttributes?.isUnsupported;
});
@@ -444,7 +441,7 @@ const avatarTooltip = computed(() => {
});
const setupHighlightTimer = () => {
if (Number(route?.query?.messageId) !== Number(props.id)) {
if (Number(route.query.messageId) !== Number(props.id)) {
return;
}
@@ -1,45 +0,0 @@
<template>
<svg
class="w-8 h-4"
viewBox="0 0 120 30"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<circle cx="15" cy="15" r="12">
<animate
attributeName="cy"
from="15"
to="5"
begin="0s"
dur="0.9s"
values="15;5;15;15;15;15"
calcMode="linear"
repeatCount="indefinite"
/>
</circle>
<circle cx="55" cy="15" r="12">
<animate
attributeName="cy"
from="15"
to="5"
begin="0.1s"
dur="0.9s"
values="15;5;15;15;15;15"
calcMode="linear"
repeatCount="indefinite"
/>
</circle>
<circle cx="95" cy="15" r="12">
<animate
attributeName="cy"
from="15"
to="5"
begin="0.2s"
dur="0.9s"
values="15;5;15;15;15;15"
calcMode="linear"
repeatCount="indefinite"
/>
</circle>
</svg>
</template>
@@ -5,7 +5,6 @@ import { useAlert } from 'dashboard/composables';
import BaseBubble from './Base.vue';
import Button from 'next/button/Button.vue';
import Icon from 'next/icon/Icon.vue';
import { useGallery } from '../useGallery';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from '../provider.js';
import { downloadFile } from '@chatwoot/utils';
@@ -22,8 +21,8 @@ const attachment = computed(() => {
});
const hasError = ref(false);
const showGallery = ref(false);
const isDownloading = ref(false);
const { showGallery, isGalleryAllowed, toggleGallery } = useGallery();
const handleError = () => {
hasError.value = true;
@@ -47,7 +46,7 @@ const downloadAttachment = async () => {
<BaseBubble
class="overflow-hidden p-3"
data-bubble-name="image"
@click="toggleGallery(true)"
@click="showGallery = true"
>
<div v-if="hasError" class="flex items-center gap-1 text-center rounded-lg">
<Icon icon="i-lucide-circle-off" class="text-n-slate-11" />
@@ -83,7 +82,7 @@ const downloadAttachment = async () => {
</div>
</BaseBubble>
<GalleryView
v-if="showGallery && isGalleryAllowed"
v-if="showGallery"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@@ -2,7 +2,6 @@
import { ref, computed } from 'vue';
import BaseBubble from './Base.vue';
import Icon from 'next/icon/Icon.vue';
import { useGallery } from '../useGallery';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from '../provider.js';
import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue';
@@ -10,7 +9,7 @@ import { ATTACHMENT_TYPES } from '../constants';
const emit = defineEmits(['error']);
const hasError = ref(false);
const { showGallery, isGalleryAllowed, toggleGallery } = useGallery();
const showGallery = ref(false);
const { filteredCurrentChatAttachments, attachments } = useMessageContext();
const handleError = () => {
@@ -31,7 +30,7 @@ const isReel = computed(() => {
<BaseBubble
class="overflow-hidden p-3"
data-bubble-name="video"
@click="toggleGallery(true)"
@click="showGallery = true"
>
<div class="relative group rounded-lg overflow-hidden">
<div
@@ -53,7 +52,7 @@ const isReel = computed(() => {
</div>
</BaseBubble>
<GalleryView
v-if="showGallery && isGalleryAllowed"
v-if="showGallery"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@@ -1,7 +1,6 @@
<script setup>
import { ref } from 'vue';
import Icon from 'next/icon/Icon.vue';
import { useGallery } from '../useGallery';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from '../provider.js';
@@ -14,7 +13,7 @@ defineProps({
},
});
const hasError = ref(false);
const { showGallery, toggleGallery, isGalleryAllowed } = useGallery();
const showGallery = ref(false);
const { filteredCurrentChatAttachments } = useMessageContext();
@@ -26,7 +25,7 @@ const handleError = () => {
<template>
<div
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer"
@click="toggleGallery(true)"
@click="showGallery = true"
>
<div
v-if="hasError"
@@ -43,7 +42,7 @@ const handleError = () => {
/>
</div>
<GalleryView
v-if="showGallery && isGalleryAllowed"
v-if="showGallery"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@@ -1,6 +1,6 @@
<script setup>
import { ref } from 'vue';
import Icon from 'next/icon/Icon.vue';
import { useGallery } from '../useGallery';
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
import { useMessageContext } from '../provider.js';
import GalleryView from 'dashboard/components/widgets/conversation/components/GalleryView.vue';
@@ -12,7 +12,7 @@ defineProps({
},
});
const { showGallery, isGalleryAllowed, toggleGallery } = useGallery();
const showGallery = ref(false);
const { filteredCurrentChatAttachments } = useMessageContext();
</script>
@@ -20,7 +20,7 @@ const { filteredCurrentChatAttachments } = useMessageContext();
<template>
<div
class="size-[72px] overflow-hidden contain-content rounded-xl cursor-pointer relative group"
@click="toggleGallery(true)"
@click="showGallery = true"
>
<video
:src="attachment.dataUrl"
@@ -42,7 +42,7 @@ const { filteredCurrentChatAttachments } = useMessageContext();
</div>
</div>
<GalleryView
v-if="showGallery && isGalleryAllowed"
v-if="showGallery"
v-model:show="showGallery"
:attachment="useSnakeCase(attachment)"
:all-attachments="filteredCurrentChatAttachments"
@@ -1,23 +0,0 @@
import { computed } from 'vue';
import { useToggle } from '@vueuse/core';
export function useGallery() {
const [showGallery] = useToggle(false);
const isGalleryAllowed = computed(() => {
return true;
// return !window.__WOOT_ISOLATED_SHELL__;
});
const toggleGallery = value => {
if (!isGalleryAllowed.value) return;
showGallery.value = value;
};
return {
showGallery,
isGalleryAllowed,
toggleGallery,
};
}
@@ -170,10 +170,6 @@ const shouldShowCannedResponses = computed(() => {
});
const editorMenuOptions = computed(() => {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ISOLATED_SHELL__) {
return [];
}
return props.enabledMenuOptions.length
? props.enabledMenuOptions
: MESSAGE_EDITOR_MENU_OPTIONS;
@@ -426,14 +422,10 @@ function updateImgToolbarOnDelete() {
}
function isEnterToSendEnabled() {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ISOLATED_SHELL__) return false;
return isEditorHotKeyEnabled('enter');
}
function isCmdPlusEnterToSendEnabled() {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ISOLATED_SHELL__) return true;
return isEditorHotKeyEnabled('cmd_enter');
}
@@ -661,7 +653,7 @@ watch(sendWithSignature, newValue => {
}
});
onMounted(async () => {
onMounted(() => {
// [VITE] state assignment was done in created before
state = createState(
props.modelValue,
@@ -742,10 +734,6 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
.ProseMirror-menubar-wrapper {
@apply flex flex-col;
.ProseMirror-menubar:empty {
display: none;
}
.ProseMirror-menubar {
min-height: var(--space-two) !important;
@apply -ml-2.5 pb-0 bg-transparent text-n-slate-11;
@@ -55,16 +55,10 @@ const translateValue = computed(() => {
class="flex items-center w-auto h-8 p-1 transition-all border rounded-full bg-n-alpha-2 group relative duration-300 ease-in-out z-0"
@click="$emit('toggleMode')"
>
<div
ref="wootEditorReplyMode"
class="flex items-center gap-1 px-2 z-20 text-n-slate-11"
>
<div ref="wootEditorReplyMode" class="flex items-center gap-1 px-2 z-20">
{{ $t('CONVERSATION.REPLYBOX.REPLY') }}
</div>
<div
ref="wootEditorPrivateMode"
class="flex items-center gap-1 px-2 z-20 text-n-slate-11"
>
<div ref="wootEditorPrivateMode" class="flex items-center gap-1 px-2 z-20">
{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}
</div>
<div
@@ -118,12 +118,6 @@ export default {
type: String,
default: '',
},
allowSignature: { type: Boolean, default: false },
allowEmoji: { type: Boolean, default: false },
allowAiAssist: { type: Boolean, default: false },
allowVideoCall: { type: Boolean, default: false },
allowFileUpload: { type: Boolean, default: false },
allowAudioRecorder: { type: Boolean, default: false },
},
emits: [
'replaceText',
@@ -270,7 +264,6 @@ export default {
<div class="flex justify-between p-3" :class="wrapClass">
<div class="left-wrap">
<NextButton
v-if="allowEmoji"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
icon="i-ph-smiley-sticker"
slate
@@ -279,7 +272,6 @@ export default {
@click="toggleEmojiPicker"
/>
<FileUpload
v-if="allowFileUpload"
ref="uploadRef"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
input-id="conversationAttachment"
@@ -303,40 +295,36 @@ export default {
sm
/>
</FileUpload>
<template v-if="allowAudioRecorder">
<NextButton
v-if="showAudioRecorderButton"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
:icon="
!isRecordingAudio ? 'i-ph-microphone' : 'i-ph-microphone-slash'
"
slate
faded
sm
@click="toggleAudioRecorder"
/>
<NextButton
v-if="showEditorToggle"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
icon="i-ph-quotes"
slate
faded
sm
@click="$emit('toggleEditor')"
/>
<NextButton
v-if="showAudioPlayStopButton"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
:icon="audioRecorderPlayStopIcon"
slate
faded
sm
:label="recordingAudioDurationText"
@click="toggleAudioRecorderPlayPause"
/>
</template>
<NextButton
v-if="allowSignature && showMessageSignatureButton"
v-if="showAudioRecorderButton"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ICON')"
:icon="!isRecordingAudio ? 'i-ph-microphone' : 'i-ph-microphone-slash'"
slate
faded
sm
@click="toggleAudioRecorder"
/>
<NextButton
v-if="showEditorToggle"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
icon="i-ph-quotes"
slate
faded
sm
@click="$emit('toggleEditor')"
/>
<NextButton
v-if="showAudioPlayStopButton"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
:icon="audioRecorderPlayStopIcon"
slate
faded
sm
:label="recordingAudioDurationText"
@click="toggleAudioRecorderPlayPause"
/>
<NextButton
v-if="showMessageSignatureButton"
v-tooltip.top-end="signatureToggleTooltip"
icon="i-ph-signature"
slate
@@ -354,15 +342,11 @@ export default {
@click="$emit('selectWhatsappTemplate')"
/>
<VideoCallButton
v-if="
allowVideoCall &&
(isAWebWidgetInbox || isAPIInbox) &&
!isOnPrivateNote
"
v-if="(isAWebWidgetInbox || isAPIInbox) && !isOnPrivateNote"
:conversation-id="conversationId"
/>
<AIAssistanceButton
v-if="allowAiAssist && !isFetchingAppIntegrations"
v-if="!isFetchingAppIntegrations"
:conversation-id="conversationId"
:is-private-note="isOnPrivateNote"
:message="message"
@@ -15,10 +15,6 @@ export default {
type: String,
default: REPLY_EDITOR_MODES.REPLY,
},
disablePopout: {
type: Boolean,
default: false,
},
isMessageLengthReachingThreshold: {
type: Boolean,
default: () => false,
@@ -89,7 +85,7 @@ export default {
</script>
<template>
<div class="flex justify-between h-[3.25rem] gap-2 ms-3">
<div class="flex justify-between h-[3.25rem] gap-2 ltr:pl-3 rtl:pr-3">
<EditorModeToggle
:mode="mode"
class="mt-3"
@@ -103,7 +99,6 @@ export default {
</div>
</div>
<NextButton
v-if="!disablePopout"
ghost
class="ltr:rounded-bl-md rtl:rounded-br-md ltr:rounded-br-none rtl:rounded-bl-none ltr:rounded-tl-none rtl:rounded-tr-none text-n-slate-11 ltr:rounded-tr-[11px] rtl:rounded-tl-[11px]"
icon="i-lucide-maximize-2"
@@ -1240,12 +1240,6 @@ export default {
:message="message"
:portal-slug="connectedPortalSlug"
:new-conversation-modal-active="newConversationModalActive"
allow-signature
allow-emoji
allow-ai-assist
allow-video-call
allow-audio-recorder
allow-file-upload
@select-whatsapp-template="openWhatsappTemplateModal"
@toggle-editor="toggleRichContentEditor"
@replace-text="replaceText"
@@ -209,7 +209,7 @@ onMounted(() => {
</div>
<div
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12 hidden sm:block"
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
>
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
</div>
@@ -2,12 +2,6 @@ import { computed, unref } from 'vue';
import { getCurrentInstance } from 'vue';
export const useStore = () => {
// eslint-disable-next-line no-underscore-dangle
if (window.__CHATWOOT_STORE__) {
// eslint-disable-next-line no-underscore-dangle
return window.__CHATWOOT_STORE__;
}
const vm = getCurrentInstance();
if (!vm) throw new Error('must be called in setup');
return vm.proxy.$store;
+3 -11
View File
@@ -10,10 +10,7 @@ const { isImpersonating } = useImpersonation();
class ActionCableConnector extends BaseActionCableConnector {
constructor(app, pubsubToken) {
const { websocketURL = '' } = window.chatwootConfig || {};
// eslint-disable-next-line no-underscore-dangle
const wsURL = websocketURL || window.__WEBSOCKET_URL__ || '';
super(app, pubsubToken, wsURL);
super(app, pubsubToken, websocketURL);
this.CancelTyping = [];
this.events = {
'message.created': this.onMessageCreated,
@@ -51,9 +48,7 @@ class ActionCableConnector extends BaseActionCableConnector {
};
isAValidEvent = data => {
// eslint-disable-next-line no-underscore-dangle
const currentAccountId = this.app.$store.getters.getCurrentAccountId;
return currentAccountId === data.account_id;
return this.app.$store.getters.getCurrentAccountId === data.account_id;
};
onMessageUpdated = data => {
@@ -103,10 +98,7 @@ class ActionCableConnector extends BaseActionCableConnector {
conversation: { last_activity_at: lastActivityAt },
conversation_id: conversationId,
} = data;
// eslint-disable-next-line no-underscore-dangle
if (!window.__WOOT_ISOLATED_SHELL__) {
DashboardAudioNotificationHelper.onNewMessage(data);
}
DashboardAudioNotificationHelper.onNewMessage(data);
this.app.$store.dispatch('addMessage', data);
this.app.$store.dispatch('updateConversationLastActivity', {
lastActivityAt,
@@ -53,9 +53,6 @@ export const getters = {
},
getCurrentAccountId(_, __, rootState) {
// eslint-disable-next-line no-underscore-dangle
if (window.__WOOT_ACCOUNT_ID__) return window.__WOOT_ACCOUNT_ID__;
if (rootState.route.params && rootState.route.params.accountId) {
return Number(rootState.route.params.accountId);
}
@@ -117,7 +114,7 @@ export const actions = {
}
},
async setUser({ commit, dispatch }) {
if (authAPI.hasAuthCookie() || authAPI.hasAuthToken()) {
if (authAPI.hasAuthCookie()) {
await dispatch('validityCheck');
} else {
commit(types.CLEAR_USER);
-56
View File
@@ -1,56 +0,0 @@
import { createApp } from 'vue';
import '../dashboard/assets/scss/app.scss';
import 'vue-multiselect/dist/vue-multiselect.css';
import 'floating-vue/dist/style.css';
// import tailwindStyles from '../dashboard/assets/scss/_woot.scss?inline';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import WootUiKit from 'dashboard/components';
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer.js';
import store from '../dashboard/store';
import constants from '../dashboard/constants/globals';
import axios from 'axios';
import createAxios from '../ui/axios';
import commonHelpers from '../dashboard/helper/commons';
import vueActionCable from '../dashboard/helper/actionCable';
import MessageList from '../ui/MessageList.vue';
import en from '../dashboard/i18n/locale/en';
import { createI18n } from 'vue-i18n';
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en,
},
});
export const init = async () => {
commonHelpers();
// eslint-disable-next-line no-underscore-dangle
window.__CHATWOOT_STORE__ = store;
window.WootConstants = constants;
window.axios = createAxios(axios);
return store.dispatch('setUser').then(() => {
const app = createApp(MessageList);
app.use(store);
app.use(i18n);
app.use(WootUiKit);
app.use(VueDOMPurifyHTML, domPurifyConfig);
// eslint-disable-next-line no-underscore-dangle
vueActionCable.init(store, window.__PUBSUB_TOKEN__);
app.mount('#app');
});
};
if (typeof window.chatwootCallback === 'function') {
window.chatwootCallback(init);
} else {
init();
}
+1 -16
View File
@@ -1,4 +1,3 @@
// scss-lint:disable SpaceAfterPropertyColon
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@@ -8,21 +7,7 @@
html,
body {
font-family:
'InterDisplay',
-apple-system,
system-ui,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Tahoma,
Arial,
sans-serif,
'Noto Sans',
'Apple Color Emoji',
'Segoe UI Emoji',
'Noto Color Emoji';
font-family: 'InterDisplay', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
height: 100%;
@@ -37,7 +37,7 @@ export default {
}
if (el.tag === 'h2') {
if (this.h1Count > 0) {
return 'ltr:ml-2 rtl:mr-2';
return 'ml-2';
}
return '';
}
@@ -46,7 +46,7 @@ export default {
if (!this.h1Count && !this.h2Count) {
return '';
}
return 'ltr:ml-5 rtl:mr-5';
return 'ml-5';
}
return '';
@@ -94,19 +94,17 @@ export default {
</script>
<template>
<div
class="hidden lg:block flex-1 py-6 scroll-mt-24 ltr:pl-4 rtl:pr-4 sticky top-24"
>
<div class="hidden lg:block flex-1 py-6 scroll-mt-24 pl-4 sticky top-24">
<div v-if="rows.length > 0" class="py-2 overflow-auto">
<nav class="max-w-2xl">
<ol
role="list"
class="flex flex-col gap-2 text-base ltr:border-l-2 rtl:border-r-2 border-solid border-slate-100 dark:border-slate-800"
class="flex flex-col gap-2 text-base border-l-2 border-solid border-slate-100 dark:border-slate-800"
>
<li
v-for="element in rows"
:key="element.slug"
class="leading-6 ltr:border-l-2 rtl:border-r-2 relative ltr:-left-0.5 rtl:-right-0.5 border-solid"
class="leading-6 border-l-2 relative -left-0.5 border-solid"
:class="elementBorderStyles(element)"
>
<p class="py-1 px-3" :class="getClassName(element)">
-10
View File
@@ -8,7 +8,6 @@ import slugifyWithCounter from '@sindresorhus/slugify';
import PublicArticleSearch from './components/PublicArticleSearch.vue';
import TableOfContents from './components/TableOfContents.vue';
import { initializeTheme } from './portalThemeHelper.js';
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
export const getHeadingsfromTheArticle = () => {
const rows = [];
@@ -115,19 +114,10 @@ export const InitializationHelpers = {
});
},
setDirectionAttribute: () => {
const portalElement = document.getElementById('portal');
if (!portalElement) return;
const locale = document.querySelector('.locale-switcher')?.value;
portalElement.dir = locale && getLanguageDirection(locale) ? 'rtl' : 'ltr';
},
initializeThemesInPortal: initializeTheme,
initialize: () => {
openExternalLinksInNewTab();
InitializationHelpers.setDirectionAttribute();
if (window.portalConfig.isPlainLayoutEnabled === 'true') {
InitializationHelpers.appendPlainParamToURLs();
} else {
@@ -1,95 +0,0 @@
# 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
@@ -1,26 +0,0 @@
import { useChatwoot } from './ChatwootProvider';
import { ChatwootMessageListWrapper } from './ChatwootMessageListWrapper';
export const ChatwootConversation = ({
conversationId,
className = '',
style = {},
...otherProps
}) => {
// Ensure we're inside a ChatwootProvider
useChatwoot(); // This will throw if not in Provider context
return (
<div
className={`chatwoot-conversation ${className} text-n-slate-11`}
style={{
height: '100%',
width: '100%',
position: 'relative',
...style,
}}
>
<ChatwootMessageListWrapper className="h-full w-full" {...otherProps} />
</div>
);
};
@@ -1,59 +0,0 @@
import { useRef, useEffect } from 'react';
export const ChatwootMessageListWrapper = ({
conversationId,
className = '',
style = {},
onError = null,
onLoad = null,
...otherProps
}) => {
const elementRef = useRef();
// Update Web Component props when React props change
useEffect(() => {
if (!elementRef.current) return;
const element = elementRef.current;
// Set conversation ID on the Web Component
element.conversationId = conversationId;
}, [conversationId]);
// Handle Web Component events
useEffect(() => {
if (!elementRef.current) return;
const element = elementRef.current;
const handleLoad = () => {
onLoad?.();
};
const handleError = (event) => {
const errorMessage = event.detail?.message || 'Unknown error occurred';
onError?.(new Error(errorMessage));
};
// Listen for custom events from the Web Component
element.addEventListener('chatwoot:loaded', handleLoad);
element.addEventListener('chatwoot:error', handleError);
return () => {
element.removeEventListener('chatwoot:loaded', handleLoad);
element.removeEventListener('chatwoot:error', handleError);
};
}, [onLoad, onError]);
// Render the Web Component
// Global setup is handled by ChatwootProvider
return (
<chatwoot-message-list
ref={elementRef}
className={className}
style={style}
{...otherProps}
/>
);
};
@@ -1,113 +0,0 @@
import { createContext, useContext, useEffect, useRef, useState } from 'react';
import { registerVueWebComponents } from '../vue-components/registerWebComponents';
import store from '../../../dashboard/store';
import constants from '../../../dashboard/constants/globals';
import axios from 'axios';
import createAxios from '../../../ui/axios';
import commonHelpers from '../../../dashboard/helper/commons';
import vueActionCable from '../../../dashboard/helper/actionCable';
const ChatwootContext = createContext();
export const ChatwootProvider = ({
baseURL,
accountId,
userToken,
websocketURL,
pubsubToken,
conversationId,
disableUpload,
disableEditor,
disablePrivateNote,
children,
}) => {
const isInitialized = useRef(false);
const [initializationComplete, setInitializationComplete] = useState(false);
// Validate required props
if (!baseURL) {
throw new Error('ChatwootProvider: baseURL is required');
}
if (!userToken) {
throw new Error('ChatwootProvider: userToken is required');
}
// Configuration object passed to all child components
const config = {
baseURL: baseURL.replace(/\/$/, ''), // Remove trailing slash
userToken,
accountId: Number(accountId),
conversationId: Number(conversationId),
disableEditor: disableEditor || false,
disablePrivateNote: disablePrivateNote || false,
disableUpload: disableUpload || false,
websocketURL: websocketURL,
pubsubToken: pubsubToken,
};
function initializeChatwootGlobals() {
// Register Web Components
registerVueWebComponents();
// Set up global variables that Vue components expect
/* eslint-disable no-underscore-dangle */
window.__WOOT_API_HOST__ = config.baseURL;
window.__WOOT_ACCOUNT_ID__ = config.accountId;
window.__WOOT_ACCESS_TOKEN__ = config.userToken;
window.__WEBSOCKET_URL__ = config.websocketURL;
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 */
// Initialize common helpers
commonHelpers();
// Set up global objects
// eslint-disable-next-line no-underscore-dangle
window.WootConstants = constants;
window.axios = createAxios(axios);
// Initialize user in store and ActionCable
store.dispatch('setUser').then(() => {
vueActionCable.init(store, config.pubsubToken);
setInitializationComplete(true);
});
}
useEffect(() => {
if (isInitialized.current) return;
initializeChatwootGlobals();
isInitialized.current = true;
}, [
config.baseURL,
config.userToken,
config.websocketURL,
config.pubsubToken,
]);
if (!initializationComplete) {
return null;
}
return (
<ChatwootContext.Provider value={config}>
{children}
</ChatwootContext.Provider>
);
};
export const useChatwoot = () => {
const context = useContext(ChatwootContext);
if (!context) {
throw new Error('useChatwoot must be used within ChatwootProvider');
}
return context;
};
// For backwards compatibility and explicit configuration access
export const useChawootConfig = useChatwoot;
@@ -1,8 +0,0 @@
// Export all React components for the Chatwoot React Components library
// Main API components
export { ChatwootProvider, useChatwoot } from './components/ChatwootProvider';
export { ChatwootConversation } from './components/ChatwootConversation';
// Lower-level components (advanced usage)
export { ChatwootMessageListWrapper } from './components/ChatwootMessageListWrapper';
@@ -1,49 +0,0 @@
<script setup>
import MessageList from '../../../ui/MessageList.vue';
</script>
<template>
<MessageList />
</template>
<style>
@import '../../../dashboard/assets/scss/app.scss';
@import 'vue-multiselect/dist/vue-multiselect.css';
@import 'floating-vue/dist/style.css';
.file-uploads {
overflow: hidden;
position: relative;
text-align: center;
display: inline-block;
}
.file-uploads.file-uploads-html4 input,
.file-uploads.file-uploads-html5 label {
/* background fix ie click */
background: #fff;
opacity: 0;
font-size: 20em;
z-index: 1;
top: 0;
left: 0;
right: 0;
bottom: 0;
position: absolute;
width: 100%;
height: 100%;
}
.file-uploads.file-uploads-html5 input,
.file-uploads.file-uploads-html4 label {
/* background fix ie click */
position: absolute;
background: rgba(255, 255, 255, 0);
overflow: hidden;
position: fixed;
width: 1px;
height: 1px;
z-index: -1;
opacity: 0;
}
</style>
@@ -1,52 +0,0 @@
import { defineCustomElement } from 'vue';
import ChatwootMessageListWebComponent from './ChatwootMessageListWebComponent.vue';
import '../../../dashboard/assets/scss/app.scss';
import en from '../../../dashboard/i18n/locale/en';
import store from '../../../dashboard/store';
import vueActionCable from '../../../dashboard/helper/actionCable';
import { createI18n, I18nInjectionKey } from 'vue-i18n';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer.js';
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en,
},
});
const ceOptions = {
configureApp(app) {
app.use(store);
app.use(VueDOMPurifyHTML, domPurifyConfig);
// I18n has to be injected inside that can be picked
// up by the compononent, the API stays the same, just use `useI18n`
// https://vue-i18n.intlify.dev/guide/advanced/wc
// Adding this link for my goldfish brain
app.use(i18n);
app.provide(I18nInjectionKey, i18n);
// eslint-disable-next-line no-underscore-dangle
vueActionCable.init(store, window.__PUBSUB_TOKEN__);
},
};
// Convert Vue components to Web Components
const ChatwootMessageListElement = defineCustomElement(
ChatwootMessageListWebComponent,
ceOptions
);
// Register Web Components
export const registerVueWebComponents = () => {
if (!customElements.get('chatwoot-message-list')) {
customElements.define('chatwoot-message-list', ChatwootMessageListElement);
// eslint-disable-next-line no-console
console.log('✅ Registered chatwoot-message-list Web Component');
}
};
// Export for manual registration if needed
export { ChatwootMessageListElement };
-538
View File
@@ -1,538 +0,0 @@
<script>
import { useTemplateRef } from 'vue';
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import inboxMixin from 'shared/mixins/inboxMixin';
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
export default {
components: {
AttachmentPreview,
ReplyTopPanel,
ReplyBottomPanel,
WootMessageEditor,
},
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
setup() {
const replyEditor = useTemplateRef('replyEditor');
return { replyEditor };
},
data() {
return {
message: '',
isFocused: false,
attachedFiles: [],
isUploading: false,
replyType: REPLY_EDITOR_MODES.REPLY,
hasSlashCommand: false,
doAutoSaveDraft: () => {},
updateEditorSelectionWith: '',
};
},
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get',
}),
currentContact() {
return this.$store.getters['contacts/getContact'](
this.currentChat.meta.sender.id
);
},
showRichContentEditor() {
return true;
},
isPrivate() {
// if the current chat is not loaded, assume we can reply
// this avoids rendering the editor with is-private yellow bg
// optimisitaclly defaulting to reply editor
if (!this.currentChat || !Object.keys(this.currentChat).length) {
return false;
}
if (this.currentChat.can_reply) {
return this.isOnPrivateNote;
}
return true;
},
inboxId() {
return this.currentChat.inbox_id;
},
inbox() {
return this.$store.getters['inboxes/getInbox'](this.inboxId);
},
messagePlaceHolder() {
return this.isPrivate
? this.$t('CONVERSATION.FOOTER.PRIVATE_MSG_INPUT')
: this.$t('CONVERSATION.FOOTER.MSG_INPUT');
},
isMessageLengthReachingThreshold() {
return this.message.length > this.maxLength - 50;
},
charactersRemaining() {
return this.maxLength - this.message.length;
},
isReplyButtonDisabled() {
if (this.hasAttachments || this.hasRecordedAudio) return false;
return (
this.isMessageEmpty ||
this.message.length === 0 ||
this.message.length > this.maxLength
);
},
sender() {
return {
name: this.currentUser.name,
thumbnail: this.currentUser.avatar_url,
};
},
conversationType() {
const { additional_attributes: additionalAttributes } = this.currentChat;
const type = additionalAttributes ? additionalAttributes.type : '';
return type || '';
},
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__;
},
allowFileUpload() {
// eslint-disable-next-line no-underscore-dangle
return window.__EDITOR_DISABLE_UPLOAD__ !== true;
},
replyButtonLabel() {
if (this.isPrivate) {
return this.$t('CONVERSATION.REPLYBOX.CREATE');
}
return this.$t('CONVERSATION.REPLYBOX.SEND');
},
replyBoxClass() {
return {
'is-private': this.isPrivate && this.allowPrivateNote,
'is-focused': this.isFocused || this.hasAttachments,
'pointer-events-none grayscale opacity-70': this.isEditorDisabled,
};
},
hasAttachments() {
return this.attachedFiles.length;
},
isOnPrivateNote() {
if (!this.allowPrivateNote) return false;
return this.replyType === REPLY_EDITOR_MODES.NOTE;
},
isOnExpandedLayout() {
return false;
},
isMessageEmpty() {
if (!this.message) {
return true;
}
return !this.message.trim().replace(/\n/g, '').length;
},
showReplyHead() {
return !this.isOnPrivateNote;
},
enableMultipleFileUpload() {
return true;
},
editorMessageKey() {
const { editor_message_key: isEnabled } = this.uiSettings;
return isEnabled;
},
conversationId() {
return this.currentChat.id;
},
editorStateId() {
return `draft-${this.conversationId}-${this.replyType}`;
},
},
mounted() {
document.addEventListener('paste', this.onPaste);
document.addEventListener('keydown', this.handleKeyEvents);
// // A hacky fix to solve the drag and drop
// // Is showing on top of new conversation modal drag and drop
// // TODO need to find a better solution
// emitter.on(
// BUS_EVENTS.NEW_CONVERSATION_MODAL,
// this.onNewConversationModalActive
// );
// emitter.on(BUS_EVENTS.INSERT_INTO_NORMAL_EDITOR, this.addIntoEditor);
},
unmounted() {
document.removeEventListener('paste', this.onPaste);
document.removeEventListener('keydown', this.handleKeyEvents);
},
methods: {
getElementToBind() {
return this.replyEditor;
},
onPaste(e) {
const data = e.clipboardData.files;
if (!this.showRichContentEditor && data.length !== 0) {
this.$refs.messageInput.$el.blur();
}
if (!data.length || !data[0]) {
return;
}
data.forEach(file => {
const { name, type, size } = file;
this.onFileUpload({ name, type, size, file: file });
});
},
confirmOnSendReply() {
if (this.isReplyButtonDisabled) {
return;
}
if (!this.showMentions) {
const messagePayload = this.getMessagePayload(this.message);
this.sendMessage(messagePayload);
this.clearMessage();
}
},
async onSendReply() {
this.confirmOnSendReply();
},
async sendMessage(messagePayload) {
try {
await this.$store.dispatch(
'createPendingMessageAndSend',
messagePayload
);
// emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
// emitter.emit(BUS_EVENTS.MESSAGE_SENT);
} catch (error) {
const errorMessage =
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
useAlert(errorMessage);
}
},
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
if (!this.allowPrivateNote) return;
const { can_reply: canReply } = this.currentChat;
this.$store.dispatch('draftMessages/setReplyEditorMode', {
mode,
});
if (canReply) this.replyType = mode;
this.$nextTick(() => {
// This block addresses a scrolling issue on iOS Safari when the virtual
// keyboard opens and can obscure the focused ProseMirror editor.
// This behavior is primarily an iOS Safari quirk related to how it handles
// the viewport with the virtual keyboard. It's not something we
// directly control or can easily fix at the component levels.
//
// `this.$nextTick()`: Ensures this code runs after Vue has finished its
// DOM update cycle. This is crucial if the editor's visibility or focus
// was just programmatically changed, guaranteeing we operate on the
// finalized DOM.
//
// `setTimeout(() => { ... }, 300)`: A delay is necessary because iOS
// Safari needs time for the virtual keyboard to fully animate into view
// and for the page layout to adjust. Attempting to scroll immediately
// (even after $nextTick) often fails as the keyboard isn't yet fully
// present or the viewport dimensions haven't updated. The 300ms is a
// common heuristic.
//
// The `scrollIntoView()` method then attempts to bring the editor
// into the visible part of the viewport. This manual scroll is a
// pragmatic workaround for this specific iOS Safari behavior.
setTimeout(() => {
document.querySelector('.ProseMirror')?.scrollIntoView();
}, 300);
});
if (this.showRichContentEditor) {
if (this.isRecordingAudio) {
this.toggleAudioRecorder();
}
return;
}
this.$nextTick(() => this.$refs.messageInput.focus());
},
clearEditorSelection() {
this.updateEditorSelectionWith = '';
},
insertIntoTextEditor(text, selectionStart, selectionEnd) {
const { message } = this;
const newMessage =
message.slice(0, selectionStart) +
text +
message.slice(selectionEnd, message.length);
this.message = newMessage;
},
addIntoEditor(content) {
if (this.showRichContentEditor) {
this.updateEditorSelectionWith = content;
this.onFocus();
}
if (!this.showRichContentEditor) {
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
this.insertIntoTextEditor(content, selectionStart, selectionEnd);
}
},
clearMessage() {
this.message = '';
this.attachedFiles = [];
this.isRecordingAudio = false;
},
onTypingOn() {
this.toggleTyping('on');
},
onTypingOff() {
this.toggleTyping('off');
},
onBlur() {
this.isFocused = false;
},
onFocus() {
this.isFocused = true;
},
toggleTyping(status) {
const conversationId = this.currentChat.id;
const isPrivate = this.isPrivate;
if (!conversationId) {
return;
}
this.$store.dispatch('conversationTypingStatus/toggleTyping', {
status,
conversationId,
isPrivate,
});
},
attachFile({ blob, file }) {
const reader = new FileReader();
reader.readAsDataURL(file.file);
reader.onloadend = () => {
this.attachedFiles.push({
currentChatId: this.currentChat.id,
resource: blob || file,
isPrivate: this.isPrivate,
thumb: reader.result,
blobSignedId: blob ? blob.signed_id : undefined,
isRecordedAudio: file?.isRecordedAudio || false,
});
};
},
removeAttachment(attachments) {
this.attachedFiles = attachments;
},
setReplyToInPayload(payload) {
if (this.inReplyTo?.id) {
return {
...payload,
contentAttributes: {
...payload.contentAttributes,
in_reply_to: this.inReplyTo.id,
},
};
}
return payload;
},
getMessagePayload(message) {
let messagePayload = {
conversationId: this.currentChat.id,
message,
private: this.isPrivate,
sender: this.sender,
};
messagePayload = this.setReplyToInPayload(messagePayload);
if (this.attachedFiles && this.attachedFiles.length) {
messagePayload.files = [];
this.attachedFiles.forEach(attachment => {
if (this.globalConfig.directUploadsEnabled) {
messagePayload.files.push(attachment.blobSignedId);
} else {
messagePayload.files.push(attachment.resource.file);
}
});
}
return messagePayload;
},
getKeyboardEvents() {
return {
'$mod+Enter': {
action: this.onSendReply,
allowOnFocusedInput: true,
},
};
},
},
};
</script>
<template>
<div ref="replyEditor" class="reply-box" :class="replyBoxClass">
<ReplyTopPanel
v-if="allowPrivateNote"
:mode="replyType"
disable-popout
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
:characters-remaining="charactersRemaining"
@set-reply-mode="setReplyMode"
/>
<div class="reply-box__top">
<WootMessageEditor
v-model="message"
:editor-id="editorStateId"
:disabled="isEditorDisabled"
class="input"
:is-private="isOnPrivateNote"
:placeholder="messagePlaceHolder"
:update-selection-with="updateEditorSelectionWith"
:min-height="4"
enable-variables
:variables="messageVariables"
:signature="signatureToApply"
allow-signature
:channel-type="channelType"
@typing-off="onTypingOff"
@typing-on="onTypingOn"
@focus="onFocus"
@blur="onBlur"
@toggle-user-mention="toggleUserMention"
@toggle-canned-menu="toggleCannedMenu"
@toggle-variables-menu="toggleVariablesMenu"
@clear-selection="clearEditorSelection"
/>
</div>
<div
v-if="hasAttachments && !showAudioRecorderEditor"
class="attachment-preview-box"
@paste="onPaste"
>
<AttachmentPreview
class="flex-col mt-4"
:attachments="attachedFiles"
@remove-attachment="removeAttachment"
/>
</div>
<ReplyBottomPanel
:conversation-id="conversationId"
:inbox="inbox"
:is-on-private-note="isOnPrivateNote"
:is-send-disabled="isReplyButtonDisabled"
:mode="replyType"
:on-file-upload="onFileUpload"
:on-send="onSendReply"
:conversation-type="conversationType"
:send-button-text="replyButtonLabel"
:show-audio-recorder="showAudioRecorder"
:show-editor-toggle="isAPIInbox && !isOnPrivateNote"
:show-emoji-picker="false"
:show-file-upload="allowFileUpload"
:message="message"
:enable-multiple-file-upload="allowFileUpload"
:allow-file-upload="allowFileUpload"
/>
</div>
</template>
<style lang="scss" scoped>
.send-button {
@apply mb-0;
}
.banner--self-assign {
@apply py-2;
}
.attachment-preview-box {
@apply bg-transparent py-0 px-4;
}
.reply-box {
transition: height 2s cubic-bezier(0.37, 0, 0.63, 1);
@apply relative mb-2 mx-2 border border-n-weak rounded-xl bg-n-solid-1;
&.is-private {
@apply bg-n-solid-amber dark:border-n-amber-3/10 border-n-amber-12/5;
}
}
/* in safari the list marker is truncated */
/* We could add, the following, but in Safari, it behaves weirdly with the curosr */
/* ::v-deep .ProseMirror li {
list-style-position: inside !important;
} */
@supports (-webkit-hyphens: none) {
::v-deep .ProseMirror ul {
margin-left: 2ch !important;
}
::v-deep .ProseMirror ol {
margin-left: 2ch !important;
}
}
.send-button {
@apply mb-0;
}
.reply-box__top {
@apply relative py-0 px-4 -mt-px;
textarea {
@apply shadow-none outline-none border-transparent bg-transparent m-0 max-h-60 min-h-[3rem] pt-4 pb-0 px-0 resize-none;
}
}
.emoji-dialog {
@apply top-[unset] -bottom-10 -left-80 right-[unset];
&::before {
transform: rotate(270deg);
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
@apply -right-4 bottom-2 rtl:right-0 rtl:-left-4;
}
}
.emoji-dialog--rtl {
@apply left-[unset] -right-80;
&::before {
transform: rotate(90deg);
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
}
}
.emoji-dialog--expanded {
@apply left-[unset] bottom-0 absolute z-[100];
&::before {
transform: rotate(0deg);
@apply left-1 -bottom-2;
}
}
.normal-editor__canned-box {
width: calc(100% - 2 * var(--space-normal));
left: var(--space-normal);
}
</style>
-149
View File
@@ -1,149 +0,0 @@
<script setup>
import { ref, onMounted, computed, watch, useTemplateRef } from 'vue';
import Message from 'next/message/Message.vue';
import TypingIndicator from 'next/message/TypingIndicator.vue';
import Snipper from 'next/spinner/Spinner.vue';
import LiteReplyBox from './LiteReplyBox.vue';
import {
useStore,
useMapGetter,
useFunctionGetter,
//
} from '../dashboard/composables/store';
import { getTypingUsersText } from '../dashboard/helper/commons';
import { useCamelCase } from '../dashboard/composables/useTransformKeys';
import { useInfiniteScroll, useThrottleFn } from '@vueuse/core';
import { useI18n } from 'vue-i18n';
const conversationId = computed(() => {
// eslint-disable-next-line no-underscore-dangle
return window.__WOOT_CONVERSATION_ID__;
});
const { t } = useI18n();
const messageListRef = useTemplateRef('messageListRef');
const store = useStore();
const isAllLoaded = useMapGetter('getAllMessagesLoaded');
const isFetching = ref(false);
const typingUserList = useFunctionGetter(
'conversationTypingStatus/getUserList',
conversationId.value
);
const isAnyoneTyping = computed(() => {
return typingUserList.value.length > 0;
});
const typingUserNames = computed(() => {
if (!isAnyoneTyping.value) return '';
const [i18nKey, params] = getTypingUsersText(typingUserList.value);
// eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys
return t(i18nKey, params);
});
const conversation = computed(() => {
return store.getters.getConversationById(conversationId.value);
});
const allMessages = computed(() => {
if (!conversation.value) return [];
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;
if (!allMessages.value?.length) return;
try {
isFetching.value = true;
await store.dispatch('fetchPreviousMessages', {
conversationId: conversation.value.id,
before: allMessages.value[allMessages.value.length - 1].id,
});
} finally {
isFetching.value = false;
}
};
onMounted(async () => {
await store.dispatch('inboxes/get');
await Promise.all([
store.dispatch('getConversation', conversationId.value),
store.dispatch('fetchAllAttachments', conversationId.value),
]);
});
watch(conversation, () => {
store.dispatch('setActiveChat', {
data: {
id: conversation.value.id,
},
});
});
useInfiniteScroll(messageListRef, useThrottleFn(fetchMore, 1000), {
canLoadMore: () => {
return !isAllLoaded.value;
},
distance: 10,
direction: 'top',
});
</script>
<template>
<div class="relative">
<ul
ref="messageListRef"
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"
id="conversationFooter"
class="my-2 py-2 flex items-center w-full"
>
<div
class="flex py-2 px-4 shadow-md rounded-full bg-white dark:bg-slate-700 text-n-slate-11 text-xs font-semibold mx-auto items-center gap-1"
>
{{ typingUserNames }}
<TypingIndicator class="text-n-slate-9" />
</div>
</div>
<template v-for="message in allMessages" :key="message.id">
<Message
v-bind="message"
:is-email-inbox="isAnEmailChannel"
:group-with-next="false"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:current-user-id="currentUserId"
/>
</template>
<div v-show="isFetching" class="w-full py-4">
<Snipper class="mx-auto" />
</div>
</ul>
<div class="p-2 w-full bg-white absolute bottom-0">
<LiteReplyBox />
</div>
</div>
</template>
<style>
/* this will disable elastic scrolling on the y axis */
* {
overscroll-behavior-y: none;
}
</style>
-19
View File
@@ -1,19 +0,0 @@
const parseErrorCode = error => Promise.reject(error);
export default axios => {
// eslint-disable-next-line no-underscore-dangle
const apiHost = window.__WOOT_API_HOST__;
// eslint-disable-next-line no-underscore-dangle
const accessToken = window.__WOOT_ACCESS_TOKEN__;
const wootApi = axios.create({ baseURL: `${apiHost}/` });
Object.assign(wootApi.defaults.headers.common, {
api_access_token: accessToken,
});
wootApi.interceptors.response.use(
response => response,
error => parseErrorCode(error)
);
return wootApi;
};
-46
View File
@@ -1,46 +0,0 @@
# Chatwoot UI Components Usage Guide
This document explains how to use the Chatwoot UI components library, which can be used both as Web Components (Custom Elements) and as Vue components.
## Installation
```bash
npm install @chatwoot/ui
# or
yarn add @chatwoot/ui
# or
pnpm add @chatwoot/ui
```
## Usage
### As Web Components (Custom Elements)
Web Components can be used in any HTML page or application, regardless of the framework.
```html
<!-- Include the built JS file -->
<script src="https://unpkg.com/@chatwoot/ui/dist/ui.js"></script>
<!-- Use the custom element anywhere in your HTML -->
<chat-button label="Click me"></chat-button>
```
When the script loads, it automatically registers all custom elements with the browser, making them immediately available for use.
#### Accessing the Store in Web Components
The components have access to a global store instance. When using Web Components, the store is available through `window.__CHATWOOT_STORE__`.
You can interact with the store from your JavaScript:
```javascript
// Access the store
const store = window.__CHATWOOT_STORE__;
// Check store state
console.log(store.state.someData);
// Dispatch actions
store.dispatch('someAction', { data: 'example' });
```
+1 -1
View File
@@ -12,7 +12,7 @@ class ContactPolicy < ApplicationPolicy
end
def export?
@account_user.administrator?
@account_user.administrator? || @account_user.agent?
end
def search?
@@ -1,10 +1,10 @@
<% author_count = category.articles.published.order(position: :asc).map(&:author).uniq.size %>
<% if author_count > 0 %>
<div class="flex flex-row items-center gap-1">
<div class="flex items-center ltr:flex-row rtl:flex-row-reverse -space-x-2">
<% category.articles.published.order(position: :asc).map(&:author).uniq.take(3).each do |author| %>
<%= render "public/api/v1/portals/thumbnail", author: author, size: 5 %>
<% end %>
<div class="flex flex-row items-center -space-x-2">
<% category.articles.published.order(position: :asc).map(&:author).uniq.take(3).each do |author| %>
<%= render "public/api/v1/portals/thumbnail", author: author, size: 5 %>
<% end %>
</div>
<% first_author = category.articles.published.order(position: :asc).map(&:author).uniq.first&.name || '' %>
@@ -3,7 +3,7 @@
<div class="flex items-center w-full py-5 overflow-hidden">
<a href="<%= generate_home_link(@portal.slug, @portal.config['default_locale'] || params[:locale], @theme_from_params, @is_plain_layout_enabled) %>" class="flex items-center h-10 text-lg font-semibold text-slate-900 dark:text-white">
<% if @portal.logo.present? %>
<img src="<%= url_for(@portal.logo) %>" class="w-auto h-10 ltr:mr-2 rtl:ml-2" />
<img src="<%= url_for(@portal.logo) %>" class="w-auto h-10 mr-2" />
<% end %>
<%= @portal.name %>
</a>
@@ -12,7 +12,7 @@
<%# Go to homepage link section %>
<div class="flex items-center justify-between gap-2 sm:gap-5">
<% if @portal.homepage_link %>
<div class="hidden px-1 py-2 ltr:ml-8 rtl:mr-8 cursor-pointer md:block">
<div class="hidden px-1 py-2 ml-8 cursor-pointer border-l-1 border-slate-50 dark:border-slate-800 md:block">
<div class="flex-grow flex-shrink-0">
<a id="header-action-button" target="_blank" rel="noopener noreferrer nofollow" href="<%= @portal.homepage_link %>" class="flex flex-row items-center gap-1 text-sm font-medium whitespace-nowrap text-slate-800 dark:text-slate-100 stroke-slate-700 dark:stroke-slate-200">
<%= render partial: 'icons/redirect' %>
@@ -42,7 +42,7 @@
</div>
</button>
<%# Appearance dropdown section %>
<div id="appearance-dropdown" data-current-theme="<%= @theme_from_params %>" class="absolute flex-col w-32 h-auto bg-white border border-solid rounded dark:bg-slate-900 top-9 ltr:right-1 rtl:left-1 border-slate-100 dark:border-slate-800" aria-hidden="true" style="display: none;" data-dropdown="appearance-dropdown">
<div id="appearance-dropdown" data-current-theme="<%= @theme_from_params %>" class="absolute flex-col w-32 h-auto bg-white border border-solid rounded dark:bg-slate-900 top-9 right-1 border-slate-100 dark:border-slate-800" aria-hidden="true" style="display: none;" data-dropdown="appearance-dropdown">
<button id="toggle-theme-button" data-theme="system" class="flex flex-row items-center justify-between gap-1 px-2 py-2 border-b border-solid border-slate-100 dark:border-slate-800 stroke-slate-700 dark:stroke-slate-200 text-slate-800 dark:text-slate-100">
<div class="flex flex-row items-center gap-1">
<%= render partial: 'icons/monitor' %>
-7
View File
@@ -13,10 +13,6 @@
"dev": "overmind start -f ./Procfile.dev",
"ruby:prettier": "bundle exec rubocop -a",
"build:sdk": "BUILD_MODE=library vite build",
"build:ui": "BUILD_MODE=ui vite build",
"build:react": "BUILD_MODE=react-components vite build",
"dev:react": "BUILD_MODE=react-components vite build --watch",
"package:react": "node scripts/package-react-components.js",
"prepare": "husky install",
"size": "size-limit",
"story:dev": "histoire dev",
@@ -119,7 +115,6 @@
"@iconify-json/teenyicons": "^1.2.1",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
"@size-limit/file": "^8.2.4",
"@vitejs/plugin-react": "^4.5.2",
"@vitest/coverage-v8": "3.0.5",
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.20",
@@ -141,8 +136,6 @@
"postcss-preset-env": "^8.5.1",
"prettier": "^3.3.3",
"prosemirror-model": "^1.22.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"size-limit": "^8.2.4",
"tailwindcss": "^3.4.13",
"vite": "^5.4.19",
-361
View File
@@ -260,9 +260,6 @@ importers:
'@size-limit/file':
specifier: ^8.2.4
version: 8.2.6(size-limit@8.2.6)
'@vitejs/plugin-react':
specifier: ^4.5.2
version: 4.5.2(vite@5.4.19(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
'@vitest/coverage-v8':
specifier: 3.0.5
version: 3.0.5(vitest@3.0.5(@types/node@22.7.0)(jsdom@24.1.3)(sass@1.79.3)(terser@5.33.0))
@@ -326,12 +323,6 @@ importers:
prosemirror-model:
specifier: ^1.22.3
version: 1.22.3
react:
specifier: ^19.1.0
version: 19.1.0
react-dom:
specifier: ^19.1.0
version: 19.1.0(react@19.1.0)
size-limit:
specifier: ^8.2.4
version: 8.2.6
@@ -372,64 +363,14 @@ packages:
'@antfu/utils@0.7.10':
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
'@babel/compat-data@7.27.5':
resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==}
engines: {node: '>=6.9.0'}
'@babel/core@7.27.4':
resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.27.5':
resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==}
engines: {node: '>=6.9.0'}
'@babel/helper-compilation-targets@7.27.2':
resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-imports@7.27.1':
resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
engines: {node: '>=6.9.0'}
'@babel/helper-module-transforms@7.27.3':
resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
'@babel/helper-plugin-utils@7.27.1':
resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.25.9':
resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.25.9':
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.27.1':
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.27.1':
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
engines: {node: '>=6.9.0'}
'@babel/helpers@7.27.6':
resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==}
engines: {node: '>=6.9.0'}
'@babel/parser@7.25.6':
resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==}
engines: {node: '>=6.0.0'}
@@ -440,23 +381,6 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.27.5':
resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/plugin-transform-react-jsx-self@7.27.1':
resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-transform-react-jsx-source@7.27.1':
resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.25.6':
resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==}
engines: {node: '>=6.9.0'}
@@ -465,22 +389,10 @@ packages:
resolution: {integrity: sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.27.4':
resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==}
engines: {node: '>=6.9.0'}
'@babel/types@7.26.0':
resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==}
engines: {node: '>=6.9.0'}
'@babel/types@7.27.6':
resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
@@ -1127,9 +1039,6 @@ packages:
'@rails/ujs@7.1.400':
resolution: {integrity: sha512-YwvXm3BR5tn+VCAKYGycLejMRVZE3Ionj5gFjEeGXCZnI0Rpi+7dKpmyu90kdUY7dRUFpHTdu9zZceEzFLl38w==}
'@rolldown/pluginutils@1.0.0-beta.11':
resolution: {integrity: sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==}
'@rollup/rollup-android-arm-eabi@4.40.2':
resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==}
cpu: [arm]
@@ -1837,18 +1746,6 @@ packages:
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
engines: {node: '>= 10'}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
'@types/babel__generator@7.27.0':
resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
'@types/babel__template@7.4.4':
resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
'@types/babel__traverse@7.20.7':
resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==}
'@types/estree@1.0.7':
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
@@ -1899,12 +1796,6 @@ packages:
'@videojs/xhr@2.6.0':
resolution: {integrity: sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==}
'@vitejs/plugin-react@4.5.2':
resolution: {integrity: sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
vite: 5.4.19
'@vitejs/plugin-vue@5.1.4':
resolution: {integrity: sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -2271,11 +2162,6 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
browserslist@4.25.0:
resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -2320,9 +2206,6 @@ packages:
caniuse-lite@1.0.30001651:
resolution: {integrity: sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==}
caniuse-lite@1.0.30001721:
resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==}
capital-case@1.0.4:
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
@@ -2449,9 +2332,6 @@ packages:
constant-case@3.0.4:
resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
core-js@3.38.1:
resolution: {integrity: sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==}
@@ -2693,9 +2573,6 @@ packages:
electron-to-chromium@1.5.13:
resolution: {integrity: sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==}
electron-to-chromium@1.5.166:
resolution: {integrity: sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==}
email-validator@2.0.4:
resolution: {integrity: sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==}
engines: {node: '>4.0'}
@@ -3071,10 +2948,6 @@ packages:
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
@@ -3130,10 +3003,6 @@ packages:
global@4.4.0:
resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
globals@11.12.0:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
engines: {node: '>=4'}
globals@13.24.0:
resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
engines: {node: '>=8'}
@@ -3508,9 +3377,6 @@ packages:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
hasBin: true
@@ -3537,11 +3403,6 @@ packages:
canvas:
optional: true
jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
hasBin: true
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
@@ -3686,9 +3547,6 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
@@ -3893,9 +3751,6 @@ packages:
node-releases@2.0.18:
resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==}
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
nopt@7.2.1:
resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -4425,19 +4280,6 @@ packages:
resolution: {integrity: sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==}
engines: {node: '>=12'}
react-dom@19.1.0:
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
peerDependencies:
react: ^19.1.0
react-refresh@0.17.0:
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
engines: {node: '>=0.10.0'}
react@19.1.0:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'}
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
@@ -4556,9 +4398,6 @@ packages:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
sdp@3.2.0:
resolution: {integrity: sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==}
@@ -4976,12 +4815,6 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
update-browserslist-db@1.1.3:
resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
upper-case-first@2.0.2:
resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==}
@@ -5317,9 +5150,6 @@ packages:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
@@ -5372,83 +5202,10 @@ snapshots:
'@antfu/utils@0.7.10': {}
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.27.1
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/compat-data@7.27.5': {}
'@babel/core@7.27.4':
dependencies:
'@ampproject/remapping': 2.3.0
'@babel/code-frame': 7.27.1
'@babel/generator': 7.27.5
'@babel/helper-compilation-targets': 7.27.2
'@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4)
'@babel/helpers': 7.27.6
'@babel/parser': 7.27.5
'@babel/template': 7.27.2
'@babel/traverse': 7.27.4
'@babel/types': 7.27.6
convert-source-map: 2.0.0
debug: 4.4.0
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
transitivePeerDependencies:
- supports-color
'@babel/generator@7.27.5':
dependencies:
'@babel/parser': 7.27.5
'@babel/types': 7.27.6
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
jsesc: 3.1.0
'@babel/helper-compilation-targets@7.27.2':
dependencies:
'@babel/compat-data': 7.27.5
'@babel/helper-validator-option': 7.27.1
browserslist: 4.25.0
lru-cache: 5.1.1
semver: 6.3.1
'@babel/helper-module-imports@7.27.1':
dependencies:
'@babel/traverse': 7.27.4
'@babel/types': 7.27.6
transitivePeerDependencies:
- supports-color
'@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)':
dependencies:
'@babel/core': 7.27.4
'@babel/helper-module-imports': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@babel/traverse': 7.27.4
transitivePeerDependencies:
- supports-color
'@babel/helper-plugin-utils@7.27.1': {}
'@babel/helper-string-parser@7.25.9': {}
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.25.9': {}
'@babel/helper-validator-identifier@7.27.1': {}
'@babel/helper-validator-option@7.27.1': {}
'@babel/helpers@7.27.6':
dependencies:
'@babel/template': 7.27.2
'@babel/types': 7.27.6
'@babel/parser@7.25.6':
dependencies:
'@babel/types': 7.26.0
@@ -5457,20 +5214,6 @@ snapshots:
dependencies:
'@babel/types': 7.26.0
'@babel/parser@7.27.5':
dependencies:
'@babel/types': 7.27.6
'@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)':
dependencies:
'@babel/core': 7.27.4
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.4)':
dependencies:
'@babel/core': 7.27.4
'@babel/helper-plugin-utils': 7.27.1
'@babel/runtime@7.25.6':
dependencies:
regenerator-runtime: 0.14.1
@@ -5479,34 +5222,11 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/parser': 7.27.5
'@babel/types': 7.27.6
'@babel/traverse@7.27.4':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/generator': 7.27.5
'@babel/parser': 7.27.5
'@babel/template': 7.27.2
'@babel/types': 7.27.6
debug: 4.4.0
globals: 11.12.0
transitivePeerDependencies:
- supports-color
'@babel/types@7.26.0':
dependencies:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@babel/types@7.27.6':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@bcoe/v8-coverage@1.0.2': {}
'@breezystack/lamejs@1.2.7': {}
@@ -6224,8 +5944,6 @@ snapshots:
'@rails/ujs@7.1.400': {}
'@rolldown/pluginutils@1.0.0-beta.11': {}
'@rollup/rollup-android-arm-eabi@4.40.2':
optional: true
@@ -7026,27 +6744,6 @@ snapshots:
'@tootallnate/once@2.0.0': {}
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.26.2
'@babel/types': 7.26.0
'@types/babel__generator': 7.27.0
'@types/babel__template': 7.4.4
'@types/babel__traverse': 7.20.7
'@types/babel__generator@7.27.0':
dependencies:
'@babel/types': 7.26.0
'@types/babel__template@7.4.4':
dependencies:
'@babel/parser': 7.26.2
'@babel/types': 7.26.0
'@types/babel__traverse@7.20.7':
dependencies:
'@babel/types': 7.26.0
'@types/estree@1.0.7': {}
'@types/flexsearch@0.7.6': {}
@@ -7105,18 +6802,6 @@ snapshots:
global: 4.4.0
is-function: 1.0.2
'@vitejs/plugin-react@4.5.2(vite@5.4.19(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
dependencies:
'@babel/core': 7.27.4
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4)
'@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.4)
'@rolldown/pluginutils': 1.0.0-beta.11
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 5.4.19(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-vue@5.1.4(vite@5.4.19(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))':
dependencies:
vite: 5.4.19(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
@@ -7609,13 +7294,6 @@ snapshots:
node-releases: 2.0.18
update-browserslist-db: 1.1.0(browserslist@4.23.3)
browserslist@4.25.0:
dependencies:
caniuse-lite: 1.0.30001721
electron-to-chromium: 1.5.166
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.0)
buffer-from@1.1.2:
optional: true
@@ -7661,8 +7339,6 @@ snapshots:
caniuse-lite@1.0.30001651: {}
caniuse-lite@1.0.30001721: {}
capital-case@1.0.4:
dependencies:
no-case: 3.0.4
@@ -7816,8 +7492,6 @@ snapshots:
tslib: 2.8.1
upper-case: 2.0.2
convert-source-map@2.0.0: {}
core-js@3.38.1: {}
countries-and-timezones@3.6.0: {}
@@ -8028,8 +7702,6 @@ snapshots:
electron-to-chromium@1.5.13: {}
electron-to-chromium@1.5.166: {}
email-validator@2.0.4: {}
emoji-regex@10.4.0: {}
@@ -8553,8 +8225,6 @@ snapshots:
functions-have-names@1.2.3: {}
gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {}
get-east-asian-width@1.2.0: {}
@@ -8633,8 +8303,6 @@ snapshots:
min-document: 2.19.0
process: 0.11.10
globals@11.12.0: {}
globals@13.24.0:
dependencies:
type-fest: 0.20.2
@@ -9035,8 +8703,6 @@ snapshots:
js-cookie@3.0.5: {}
js-tokens@4.0.0: {}
js-yaml@3.14.1:
dependencies:
argparse: 1.0.10
@@ -9107,8 +8773,6 @@ snapshots:
- supports-color
- utf-8-validate
jsesc@3.1.0: {}
json-buffer@3.0.1: {}
json-logic-js@2.0.5: {}
@@ -9267,10 +8931,6 @@ snapshots:
lru-cache@10.4.3: {}
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
lru-cache@6.0.0:
dependencies:
yallist: 4.0.0
@@ -9467,8 +9127,6 @@ snapshots:
node-releases@2.0.18: {}
node-releases@2.0.19: {}
nopt@7.2.1:
dependencies:
abbrev: 2.0.0
@@ -10048,15 +9706,6 @@ snapshots:
quick-lru@6.1.2: {}
react-dom@19.1.0(react@19.1.0):
dependencies:
react: 19.1.0
scheduler: 0.26.0
react-refresh@0.17.0: {}
react@19.1.0: {}
read-cache@1.0.0:
dependencies:
pify: 2.3.0
@@ -10204,8 +9853,6 @@ snapshots:
dependencies:
xmlchars: 2.2.0
scheduler@0.26.0: {}
sdp@3.2.0: {}
section-matter@1.0.0:
@@ -10666,12 +10313,6 @@ snapshots:
escalade: 3.2.0
picocolors: 1.0.1
update-browserslist-db@1.1.3(browserslist@4.25.0):
dependencies:
browserslist: 4.25.0
escalade: 3.2.0
picocolors: 1.1.1
upper-case-first@2.0.2:
dependencies:
tslib: 2.8.1
@@ -11024,8 +10665,6 @@ snapshots:
y18n@5.0.8: {}
yallist@3.1.1: {}
yallist@4.0.0: {}
yaml-eslint-parser@1.2.3:
-61
View File
@@ -1,61 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Components Test</title>
<script>
window.__WOOT_ISOLATED_SHELL__ = true;
window.__WOOT_ACCOUNT_ID__ = 1;
window.__WOOT_API_HOST__ = 'http://localhost:3000';
window.__WOOT_ACCESS_TOKEN__ = 'token';
window.__PUBSUB_TOKEN__ = 'token';
window.__WEBSOCKET_URL__ = 'ws://localhost:3000';
window.__WOOT_CONVERSATION_ID__ = 13918;
window.__DISABLE_EDITOR__ = true;
window.__EDITOR_DISABLE_UPLOAD__ = true;
</script>
</head>
<body>
<div id="app" dir="ltr">
<div class="py-20 grid place-content-center">
<svg
width="40"
height="40"
viewBox="0 0 40 40"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="20"
cy="20"
r="18"
fill="none"
stroke="#ccc"
stroke-width="4"
/>
<circle
cx="20"
cy="20"
r="18"
fill="none"
stroke="#333"
stroke-width="4"
stroke-dasharray="56.5487 56.5487"
stroke-dashoffset="0"
>
<animateTransform
attributeName="transform"
type="rotate"
from="0 20 20"
to="360 20 20"
dur="1s"
repeatCount="indefinite"
/>
</circle>
</svg>
</div>
</div>
<link rel="stylesheet" href="/packs/style.css" />
<script src="/packs/js/ui.js"></script>
</body>
</html>
-184
View File
@@ -1,184 +0,0 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
/* eslint-disable no-console */
// Check for copy-only mode
const isCopyMode = process.argv.includes('--copyMode');
if (isCopyMode) {
console.log('📋 Copy mode: Copying built files to dist/react-components...');
} else {
console.log('🚀 Building Chatwoot React Components for NPM...');
}
function copyBuildFiles(packageDir) {
// Copy main build outputs
const files = [
{ src: 'public/packs/react-components.es.js', dest: 'index.js' }, // ES module (main)
{ src: 'public/packs/react-components.cjs.js', dest: 'index.cjs' }, // CommonJS
{ src: 'public/packs/style.css', dest: 'style.css' }, // CSS styles
];
files.forEach(({ src, dest }) => {
if (fs.existsSync(src)) {
fs.copyFileSync(src, path.join(packageDir, dest));
console.log(` ✓ Copied ${dest}`);
} else {
console.warn(` ⚠️ Warning: ${src} not found`);
}
});
}
function generatePackageJson(packageDir) {
// Read version from main package.json
const mainPackage = JSON.parse(fs.readFileSync('package.json', 'utf8'));
// 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();
// 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 generate timestamp version, using incremented patch'
);
const [major, minor, patch] = mainPackage.version.split('.').map(Number);
return `${major}.${minor}.${patch + 1}`;
}
}
const finalVersion = generateTimestampVersion();
console.log(` 📋 Package version: ${finalVersion}`);
const packageJson = {
name: '@chatwoot/agent-react-components',
version: finalVersion,
description:
'React components for Chatwoot messaging interface with Vue Web Components',
// Entry points for different module systems
main: 'index.cjs', // CommonJS entry point for Node.js
module: 'index.js', // ES module entry point for bundlers
exports: {
'.': {
import: './index.js', // ES modules
require: './index.cjs', // CommonJS
},
'./style.css': './style.css',
},
// Peer dependencies - what the consuming app must provide
peerDependencies: {
react: '>=16.8.0', // Hooks support
'react-dom': '>=16.8.0',
},
// Package metadata
keywords: [
'chatwoot',
'react',
'vue',
'webcomponents',
'chat',
'messaging',
'customer-support',
'ui-components',
],
author: 'Chatwoot',
license: 'MIT',
repository: {
type: 'git',
url: 'https://github.com/chatwoot/chatwoot.git',
directory: 'app/javascript/react-components',
},
homepage: 'https://chatwoot.com',
bugs: {
url: 'https://github.com/chatwoot/chatwoot/issues',
},
// npm publish configuration
files: ['index.js', 'index.cjs', 'style.css', 'package.json'],
// Engine requirements
engines: {
node: '>=16.0.0',
},
};
fs.writeFileSync(
path.join(packageDir, 'package.json'),
JSON.stringify(packageJson, null, 2)
);
console.log(' ✓ Generated package.json');
}
async function publishReactComponents() {
try {
if (!isCopyMode) {
// Step 1: Clean previous builds
console.log('📦 Cleaning previous builds...');
if (fs.existsSync('dist')) {
fs.rmSync('dist', { recursive: true, force: true });
}
// Clean previous React component builds
const reactFiles = [
'public/packs/react-components.es.js',
'public/packs/react-components.cjs.js',
'public/packs/style.css',
];
reactFiles.forEach(file => {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
console.log(` 🗑️ Removed ${file}`);
}
});
// Step 2: Build the React components library
console.log('🔨 Building React components...');
execSync('pnpm build:react', { stdio: 'inherit' });
}
// Step 3: Create package directory
console.log('📁 Creating package directory...');
const packageDir = 'dist/react-components';
fs.mkdirSync(packageDir, { recursive: true });
// Step 4: Copy built files
console.log('📋 Copying built files...');
copyBuildFiles(packageDir);
// Step 5: Generate package.json
console.log('📄 Generating package.json...');
generatePackageJson(packageDir);
console.log('✅ Package ready in dist/react-components/');
console.log('');
console.log('📦 Publishing options:');
console.log(' • npm publish: cd dist/react-components && npm publish');
console.log(' • yalc publish: pnpm package:react:yalc-publish');
console.log(' • local test: cd dist/react-components && npm pack');
} catch (error) {
console.error('❌ Build failed:', error.message);
process.exit(1);
}
}
// Run the script
publishReactComponents();
/* eslint-enable no-console */
-3
View File
@@ -29,13 +29,10 @@ const tailwindConfig = {
'./app/javascript/portal/**/*.vue',
'./app/javascript/shared/**/*.vue',
'./app/javascript/survey/**/*.vue',
'./app/javascript/ui/**/*.vue',
'./app/javascript/dashboard/components-next/**/*.vue',
'./app/javascript/dashboard/helper/**/*.js',
'./app/javascript/dashboard/components-next/**/*.js',
'./app/javascript/dashboard/routes/dashboard/**/**/*.js',
'./app/javascript/react-components/**/*.{js,jsx,vue}',
'./app/javascript/dashboard/assets/scss/**/*.scss',
'./app/views/**/*.html.erb',
],
theme: {
+15 -65
View File
@@ -22,11 +22,8 @@ import { defineConfig } from 'vite';
import ruby from 'vite-plugin-ruby';
import path from 'path';
import vue from '@vitejs/plugin-vue';
import react from '@vitejs/plugin-react';
const isLibraryMode = process.env.BUILD_MODE === 'library';
const uiMode = process.env.BUILD_MODE === 'ui';
const reactComponentMode = process.env.BUILD_MODE === 'react-components';
const isTestMode = process.env.TEST === 'true';
const vueOptions = {
@@ -41,85 +38,38 @@ let plugins = [ruby(), vue(vueOptions)];
if (isLibraryMode) {
plugins = [];
} else if (uiMode) {
plugins = [vue()];
} else if (reactComponentMode) {
plugins = [vue({ ...vueOptions, customElement: true }), react()];
} else if (isTestMode) {
plugins = [vue(vueOptions)];
}
let lib;
let rollupOptions = {};
if (isLibraryMode) {
lib = {
entry: path.resolve(__dirname, './app/javascript/entrypoints/sdk.js'),
formats: ['iife'], // IIFE format for single file
name: 'sdk',
};
} else if (uiMode) {
lib = {
entry: path.resolve(__dirname, './app/javascript/entrypoints/ui.js'),
formats: ['iife'], // IIFE format for single file
name: 'ui',
};
} else if (reactComponentMode) {
lib = {
entry: path.resolve(
__dirname,
'./app/javascript/react-components/src/index.jsx'
),
formats: ['es', 'cjs'], // ES modules and CommonJS only
fileName: format => `react-components.${format}.js`,
};
rollupOptions = {
external: ['react', 'react-dom'],
};
}
const chunkBuilder = chunkInfo => {
if (chunkInfo.name === 'sdk') {
return 'js/sdk.js';
}
if (chunkInfo.name === 'ui') {
return `js/ui.js`;
}
// For react components, we need to return different names but can't access format here
// So we'll handle this differently
return '[name].js';
};
export default defineConfig({
plugins: plugins,
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
build: {
rollupOptions: {
...rollupOptions,
output: {
...rollupOptions.output,
// [NOTE] when not in library mode, no new keys will be addedd or overwritten
// setting dir: isLibraryMode ? 'public/packs' : undefined will not work
...(isLibraryMode || uiMode
...(isLibraryMode
? {
dir: 'public/packs',
entryFileNames: chunkBuilder,
entryFileNames: chunkInfo => {
if (chunkInfo.name === 'sdk') {
return 'js/sdk.js';
}
return '[name].js';
},
}
: {}),
...(reactComponentMode
? {
dir: 'public/packs',
}
: {}),
inlineDynamicImports: isLibraryMode || uiMode || reactComponentMode, // Disable code-splitting for SDK
inlineDynamicImports: isLibraryMode, // Disable code-splitting for SDK
},
},
lib,
lib: isLibraryMode
? {
entry: path.resolve(__dirname, './app/javascript/entrypoints/sdk.js'),
formats: ['iife'], // IIFE format for single file
name: 'sdk',
}
: undefined,
},
resolve: {
alias: {