Compare commits

...
Author SHA1 Message Date
Sivin VargheseandGitHub b7d23d9435 Merge branch 'develop' into feat/open-article-endpoint 2026-06-10 23:32:50 +05:30
Shivam MishraandGitHub 9f64d50011 Merge branch 'develop' into feat/open-article-endpoint 2026-06-10 16:40:34 +05:30
Shivam Mishra f926f5e500 fix: navigating once an article is open 2026-06-10 14:33:35 +05:30
Shivam MishraandGitHub c9f6fb202c Merge branch 'develop' into feat/open-article-endpoint 2026-06-10 14:12:41 +05:30
Shivam MishraandGitHub 7830fec604 Merge branch 'develop' into feat/open-article-endpoint 2026-06-09 22:33:34 +05:30
Shivam MishraandGitHub ba04e0b678 Merge branch 'develop' into feat/open-article-endpoint 2026-06-04 13:02:20 +05:30
Shivam Mishra 8625a00918 feat: open help center article from sdk
Add window.$chatwoot.openArticle(slug) which opens the widget and navigates the in-widget article viewer to the given help center article.
2026-06-02 16:01:09 +05:30
Shivam Mishra 16e638aeb7 refactor: extract article viewer link builder
Move the article viewer query-param logic out of ArticleContainer into a shared buildArticleViewerLink helper so it can be reused.
2026-06-02 16:01:00 +05:30
5 changed files with 70 additions and 9 deletions
+9
View File
@@ -109,6 +109,15 @@ const runSDK = ({ baseUrl, websiteToken }) => {
});
},
openArticle(slug) {
if (!slug) {
throw new Error('Article slug is required');
}
IFrameHelper.events.toggleBubble('open');
IFrameHelper.sendMessage('open-article', { slug });
},
setUser(identifier, user) {
if (typeof identifier !== 'string' && typeof identifier !== 'number') {
throw new Error('Identifier should be a string or a number');
@@ -38,3 +38,24 @@ export const getMatchingLocale = (selectedLocale = '', allowedLocales = []) => {
// Return the first match that exists in the allowed list, or null
return priorityMatches.find(l => l && allowedLocales.includes(l)) ?? null;
};
/**
* Build the link consumed by the in-widget article viewer, appending the query
* params it expects (plain layout, theme and locale).
*
* @export
* @param {Object} options
* @param {string} options.link Relative article/portal path (e.g. `hc/slug/articles/foo`).
* @param {(string|null)} [options.locale] Resolved portal locale.
* @param {boolean} [options.prefersDarkMode] Whether the widget is in dark mode.
* @returns {string} The link with the article viewer query params appended.
*/
export const buildArticleViewerLink = ({ link, locale, prefersDarkMode }) => {
const params = new URLSearchParams({
show_plain_layout: 'true',
theme: prefersDarkMode ? 'dark' : 'light',
...(locale && { locale }),
});
return `${link}?${params.toString()}`;
};
+26
View File
@@ -22,6 +22,10 @@ import { useRouter } from 'vue-router';
import { useAvailability } from 'widget/composables/useAvailability';
import { SDK_SET_BUBBLE_VISIBILITY } from '../shared/constants/sharedFrameEvents';
import { emitter } from 'shared/helpers/mitt';
import {
getMatchingLocale,
buildArticleViewerLink,
} from 'shared/helpers/portalHelper';
export default {
name: 'App',
@@ -160,6 +164,26 @@ export default {
this.$root.$i18n.locale = localeWithoutVariation;
}
},
openArticle(slug) {
const { portal } = window.chatwootWebChannel;
if (!portal || !slug) return;
const locale = getMatchingLocale(
this.$root.$i18n.locale,
portal.config?.allowed_locales
);
const link = buildArticleViewerLink({
link: `hc/${portal.slug}/articles/${slug}`,
locale,
prefersDarkMode: this.prefersDarkMode,
});
// Add a timestamp so the route always changes, even when the same article
// is requested again or the iframe was browsed to another page.
this.router.push({
name: 'article-viewer',
query: { link, v: Date.now() },
});
},
registerUnreadEvents() {
emitter.on(ON_AGENT_MESSAGE_RECEIVED, () => {
const { name: routeName } = this.$route;
@@ -316,6 +340,8 @@ export default {
this.setBubbleLabel();
} else if (message.event === 'set-color-scheme') {
this.setColorScheme(message.darkMode);
} else if (message.event === 'open-article') {
this.openArticle(message.slug);
} else if (message.event === 'toggle-open') {
this.$store.dispatch('appConfig/toggleWidgetOpen', message.isOpen);
@@ -7,7 +7,10 @@ import { useRouter } from 'vue-router';
import { useStore } from 'dashboard/composables/store';
import { useMapGetter } from 'dashboard/composables/store.js';
import { useDarkMode } from 'widget/composables/useDarkMode';
import { getMatchingLocale } from 'shared/helpers/portalHelper';
import {
getMatchingLocale,
buildArticleViewerLink,
} from 'shared/helpers/portalHelper';
const store = useStore();
const router = useRouter();
@@ -38,14 +41,11 @@ const fetchArticles = () => {
};
const openArticleInArticleViewer = link => {
const params = new URLSearchParams({
show_plain_layout: 'true',
theme: prefersDarkMode.value ? 'dark' : 'light',
...(locale.value && { locale: locale.value }),
const linkToOpen = buildArticleViewerLink({
link,
locale: locale.value,
prefersDarkMode: prefersDarkMode.value,
});
// Combine link with query parameters
const linkToOpen = `${link}?${params.toString()}`;
router.push({ name: 'article-viewer', query: { link: linkToOpen } });
};
@@ -11,6 +11,11 @@ export default {
<template>
<div class="bg-white h-full">
<IframeLoader :url="$route.query.link" />
<!--
Key by fullPath (not just the link) so the iframe remounts on every
navigation here, including re-opening the same article via the SDK after
the iframe was browsed to another help-center page. See App.vue#openArticle.
-->
<IframeLoader :key="$route.fullPath" :url="$route.query.link" />
</div>
</template>