Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,113 @@
import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
import axios from 'axios';
// Global Components
import hljsVuePlugin from '@highlightjs/vue-plugin';
import { plugin, defaultConfig } from '@formkit/vue';
import WootWizard from 'components/ui/Wizard.vue';
import FloatingVue from 'floating-vue';
import WootUiKit from 'dashboard/components';
import App from 'dashboard/App.vue';
import i18nMessages from 'dashboard/i18n';
import createAxios from 'dashboard/helper/APIHelper';
import commonHelpers, { isJSONValid } from 'dashboard/helper/commons';
import { sync } from 'vuex-router-sync';
import { createPinia } from 'pinia';
import router, { initalizeRouter } from 'dashboard/routes';
import store from 'dashboard/store';
import constants from 'dashboard/constants/globals';
import * as Sentry from '@sentry/vue';
import {
initializeAnalyticsEvents,
initializeChatwootEvents,
} from 'dashboard/helper/scriptHelpers.js';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer.js';
import { vResizeObserver } from '@vueuse/components';
import { directive as onClickaway } from 'vue3-click-away';
import 'floating-vue/dist/style.css';
const i18n = createI18n({
legacy: false, // https://github.com/intlify/vue-i18n/issues/1902
locale: 'en',
messages: i18nMessages,
});
sync(store, router);
const pinia = createPinia();
const app = createApp(App);
app.use(i18n);
app.use(store);
app.use(pinia);
app.use(router);
// [VITE] Disabled this, need to renable later
if (window.errorLoggingConfig) {
Sentry.init({
app,
dsn: window.errorLoggingConfig,
denyUrls: [
// Chrome extensions
/^chrome:\/\//i,
/chrome-extension:/i,
/extensions\//i,
// Locally saved copies
/file:\/\//i,
// Safari extensions.
/safari-web-extension:/i,
/safari-extension:/i,
],
integrations: [Sentry.browserTracingIntegration({ router })],
ignoreErrors: [
'ResizeObserver loop completed with undelivered notifications',
],
});
}
app.use(VueDOMPurifyHTML, domPurifyConfig);
app.use(WootUiKit);
app.use(
plugin,
defaultConfig({
rules: {
JSON: ({ value }) => isJSONValid(value),
},
})
);
app.use(FloatingVue, {
instantMove: true,
arrowOverflow: false,
disposeTimeout: 5000000,
});
app.use(hljsVuePlugin);
app.component('woot-wizard', WootWizard);
app.component('fluent-icon', FluentIcon);
app.directive('resize', vResizeObserver);
app.directive('on-clickaway', onClickaway);
// load common helpers into js
commonHelpers();
window.WootConstants = constants;
window.axios = createAxios(axios);
// [VITE] Disabled this we don't need it, we can use `useEmitter` directly
// app.prototype.$emitter = emitter;
initializeChatwootEvents();
initializeAnalyticsEvents();
initalizeRouter();
window.onload = () => {
app.mount('#app');
};

View File

@@ -0,0 +1,9 @@
import Rails from '@rails/ujs';
import Turbolinks from 'turbolinks';
import '../portal/application.scss';
import { InitializationHelpers } from '../portal/portalHelpers';
Rails.start();
Turbolinks.start();
document.addEventListener('turbolinks:load', InitializationHelpers.onLoad);

View File

@@ -0,0 +1,221 @@
import Cookies from 'js-cookie';
import { IFrameHelper } from '../sdk/IFrameHelper';
import {
getBubbleView,
getDarkMode,
getWidgetStyle,
} from '../sdk/settingsHelper';
import {
computeHashForUserData,
getUserCookieName,
hasUserKeys,
} from '../sdk/cookieHelpers';
import {
addClasses,
removeClasses,
restoreWidgetInDOM,
} from '../sdk/DOMHelpers';
import { setCookieWithDomain } from '../sdk/cookieHelpers';
import { SDK_SET_BUBBLE_VISIBILITY } from 'shared/constants/sharedFrameEvents';
const runSDK = ({ baseUrl, websiteToken }) => {
if (window.$chatwoot) {
return;
}
// if this is a Rails Turbo app
document.addEventListener('turbo:before-render', event => {
// when morphing the page, this typically happens on reload like events
// say you update a "Customer" on a form and it reloads the page
// We have already added data-turbo-permananent to true. This
// will ensure that the widget it preserved
// Read more about morphing here: https://turbo.hotwired.dev/handbook/page_refreshes#morphing
// and peristing elements here: https://turbo.hotwired.dev/handbook/building#persisting-elements-across-page-loads
if (event.detail.renderMethod === 'morph') return;
restoreWidgetInDOM(event.detail.newBody);
});
if (window.Turbolinks) {
document.addEventListener('turbolinks:before-render', event => {
restoreWidgetInDOM(event.data.newBody);
});
}
// if this is an astro app
document.addEventListener('astro:before-swap', event =>
restoreWidgetInDOM(event.newDocument.body)
);
const chatwootSettings = window.chatwootSettings || {};
let locale = chatwootSettings.locale;
let baseDomain = chatwootSettings.baseDomain;
if (chatwootSettings.useBrowserLanguage) {
locale = window.navigator.language.replace('-', '_');
}
window.$chatwoot = {
baseUrl,
baseDomain,
hasLoaded: false,
hideMessageBubble: chatwootSettings.hideMessageBubble || false,
isOpen: false,
position: chatwootSettings.position === 'left' ? 'left' : 'right',
websiteToken,
locale,
useBrowserLanguage: chatwootSettings.useBrowserLanguage || false,
type: getBubbleView(chatwootSettings.type),
launcherTitle: chatwootSettings.launcherTitle || '',
showPopoutButton: chatwootSettings.showPopoutButton || false,
showUnreadMessagesDialog: chatwootSettings.showUnreadMessagesDialog ?? true,
widgetStyle: getWidgetStyle(chatwootSettings.widgetStyle) || 'standard',
resetTriggered: false,
darkMode: getDarkMode(chatwootSettings.darkMode),
welcomeTitle: chatwootSettings.welcomeTitle || '',
welcomeDescription: chatwootSettings.welcomeDescription || '',
availableMessage: chatwootSettings.availableMessage || '',
unavailableMessage: chatwootSettings.unavailableMessage || '',
enableFileUpload: chatwootSettings.enableFileUpload,
enableEmojiPicker: chatwootSettings.enableEmojiPicker ?? true,
enableEndConversation: chatwootSettings.enableEndConversation ?? true,
toggle(state) {
IFrameHelper.events.toggleBubble(state);
},
toggleBubbleVisibility(visibility) {
let widgetElm = document.querySelector('.woot--bubble-holder');
let widgetHolder = document.querySelector('.woot-widget-holder');
if (visibility === 'hide') {
addClasses(widgetHolder, 'woot-widget--without-bubble');
addClasses(widgetElm, 'woot-hidden');
window.$chatwoot.hideMessageBubble = true;
} else if (visibility === 'show') {
removeClasses(widgetElm, 'woot-hidden');
removeClasses(widgetHolder, 'woot-widget--without-bubble');
window.$chatwoot.hideMessageBubble = false;
}
IFrameHelper.sendMessage(SDK_SET_BUBBLE_VISIBILITY, {
hideMessageBubble: window.$chatwoot.hideMessageBubble,
});
},
popoutChatWindow() {
IFrameHelper.events.popoutChatWindow({
baseUrl: window.$chatwoot.baseUrl,
websiteToken: window.$chatwoot.websiteToken,
locale,
});
},
setUser(identifier, user) {
if (typeof identifier !== 'string' && typeof identifier !== 'number') {
throw new Error('Identifier should be a string or a number');
}
if (!hasUserKeys(user)) {
throw new Error(
'User object should have one of the keys [avatar_url, email, name]'
);
}
const userCookieName = getUserCookieName();
const existingCookieValue = Cookies.get(userCookieName);
const hashToBeStored = computeHashForUserData({ identifier, user });
if (hashToBeStored === existingCookieValue) {
return;
}
window.$chatwoot.identifier = identifier;
window.$chatwoot.user = user;
IFrameHelper.sendMessage('set-user', { identifier, user });
setCookieWithDomain(userCookieName, hashToBeStored, {
baseDomain,
});
},
setCustomAttributes(customAttributes = {}) {
if (!customAttributes || !Object.keys(customAttributes).length) {
throw new Error('Custom attributes should have atleast one key');
} else {
IFrameHelper.sendMessage('set-custom-attributes', { customAttributes });
}
},
deleteCustomAttribute(customAttribute = '') {
if (!customAttribute) {
throw new Error('Custom attribute is required');
} else {
IFrameHelper.sendMessage('delete-custom-attribute', {
customAttribute,
});
}
},
setConversationCustomAttributes(customAttributes = {}) {
if (!customAttributes || !Object.keys(customAttributes).length) {
throw new Error('Custom attributes should have atleast one key');
} else {
IFrameHelper.sendMessage('set-conversation-custom-attributes', {
customAttributes,
});
}
},
deleteConversationCustomAttribute(customAttribute = '') {
if (!customAttribute) {
throw new Error('Custom attribute is required');
} else {
IFrameHelper.sendMessage('delete-conversation-custom-attribute', {
customAttribute,
});
}
},
setLabel(label = '') {
IFrameHelper.sendMessage('set-label', { label });
},
removeLabel(label = '') {
IFrameHelper.sendMessage('remove-label', { label });
},
setLocale(localeToBeUsed = 'en') {
IFrameHelper.sendMessage('set-locale', { locale: localeToBeUsed });
},
setColorScheme(darkMode = 'light') {
IFrameHelper.sendMessage('set-color-scheme', {
darkMode: getDarkMode(darkMode),
});
},
reset() {
if (window.$chatwoot.isOpen) {
IFrameHelper.events.toggleBubble();
}
Cookies.remove('cw_conversation');
Cookies.remove(getUserCookieName());
const iframe = IFrameHelper.getAppFrame();
iframe.src = IFrameHelper.getUrl({
baseUrl: window.$chatwoot.baseUrl,
websiteToken: window.$chatwoot.websiteToken,
});
window.$chatwoot.resetTriggered = true;
},
};
IFrameHelper.createFrame({
baseUrl,
websiteToken,
});
};
window.chatwootSDK = {
run: runSDK,
};

View File

@@ -0,0 +1 @@
import '../dashboard/assets/scss/super_admin/index.scss';

View File

@@ -0,0 +1,34 @@
import 'chart.js';
import { createApp, h } from 'vue';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import PlaygroundIndex from '../superadmin_pages/views/playground/Index.vue';
import DashboardIndex from '../superadmin_pages/views/dashboard/Index.vue';
const ComponentMapping = {
PlaygroundIndex: PlaygroundIndex,
DashboardIndex: DashboardIndex,
};
const renderComponent = (componentName, props) => {
const app = createApp({
data() {
return { props: props };
},
render() {
return h(ComponentMapping[componentName], { componentData: this.props });
},
});
app.use(VueDOMPurifyHTML);
app.mount('#app');
};
document.addEventListener('DOMContentLoaded', () => {
const element = document.getElementById('app');
if (element) {
const componentName = element.dataset.componentName;
const props = JSON.parse(element.dataset.props);
renderComponent(componentName, props);
}
});

View File

@@ -0,0 +1,18 @@
import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
import store from '../survey/store';
import i18nMessages from '../survey/i18n';
import App from '../survey/App.vue';
const app = createApp(App);
const i18n = createI18n({
locale: 'en',
messages: i18nMessages,
});
app.use(i18n);
app.use(store);
window.onload = () => {
window.WOOT_SURVEY = app.mount('#app');
};

View File

@@ -0,0 +1,66 @@
import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
import i18nMessages from 'dashboard/i18n';
import * as Sentry from '@sentry/vue';
import {
initializeAnalyticsEvents,
initializeChatwootEvents,
} from 'dashboard/helper/scriptHelpers';
import App from '../v3/App.vue';
import router, { initalizeRouter } from '../v3/views/index';
import store from '../v3/store';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
// import { emitter } from '../shared/helpers/mitt';
// [VITE] This was added in https://github.com/chatwoot/chatwoot/commit/b57063a8b83c86819bd285f481298d7cd38ad50e
// Commenting it out for Vite migration
// Vue.config.env = process.env;
const i18n = createI18n({
legacy: false, // https://github.com/intlify/vue-i18n/issues/1902
locale: 'en',
messages: i18nMessages,
});
const app = createApp(App);
app.use(i18n);
app.use(store);
app.use(router);
// Vue.use(VueRouter);
// Vue.use(VueI18n);
// Vue.prototype.$emitter = emitter;
app.component('fluent-icon', FluentIcon);
if (window.errorLoggingConfig) {
Sentry.init({
app,
dsn: window.errorLoggingConfig,
denyUrls: [
// Chrome extensions
/^chrome:\/\//i,
/chrome-extension:/i,
/extensions\//i,
// Locally saved copies
/file:\/\//i,
// Safari extensions.
/safari-web-extension:/i,
/safari-extension:/i,
],
integrations: [Sentry.browserTracingIntegration({ router })],
ignoreErrors: [
'ResizeObserver loop completed with undelivered notifications',
],
});
}
initializeChatwootEvents();
initializeAnalyticsEvents();
initalizeRouter();
window.onload = () => {
app.mount('#app');
};

View File

@@ -0,0 +1,54 @@
import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import store from '../widget/store';
import App from '../widget/App.vue';
import ActionCableConnector from '../widget/helpers/actionCable';
import i18nMessages from '../widget/i18n';
import router from '../widget/router';
import { directive as onClickaway } from 'vue3-click-away';
import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer';
import { plugin, defaultConfig } from '@formkit/vue';
import {
startsWithPlus,
isPhoneNumberValidWithDialCode,
} from 'shared/helpers/Validators';
const i18n = createI18n({
legacy: false, // https://github.com/intlify/vue-i18n/issues/1902
locale: 'en',
messages: i18nMessages,
});
const app = createApp(App);
app.use(i18n);
app.use(store);
app.use(router);
app.use(VueDOMPurifyHTML, domPurifyConfig);
app.directive('on-clickaway', onClickaway);
app.use(
plugin,
defaultConfig({
rules: {
startsWithPlus: ({ value }) => startsWithPlus(value),
isValidPhoneNumber: ({ value }) => isPhoneNumberValidWithDialCode(value),
},
})
);
// Event Bus
// We can use the useEmitter directly
// Vue.prototype.$emitter = emitter;
// Vue.config.productionTip = false;
window.onload = () => {
window.WOOT_WIDGET = app.mount('#app');
window.actionCable = new ActionCableConnector(
window.WOOT_WIDGET,
window.chatwootPubsubToken
);
};