Migrate widgetPortalArticlesToPinia

This commit is contained in:
Pranav
2025-01-12 12:03:35 -08:00
parent bb632d563c
commit ecb99e0e8c
12 changed files with 216 additions and 155 deletions
+3
View File
@@ -10,6 +10,7 @@ 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 { createPinia } from 'pinia';
import {
startsWithPlus,
@@ -21,9 +22,11 @@ const i18n = createI18n({
locale: 'en',
messages: i18nMessages,
});
const pinia = createPinia();
const app = createApp(App);
app.use(i18n);
app.use(pinia);
app.use(store);
app.use(router);
app.use(VueDOMPurifyHTML, domPurifyConfig);
-7
View File
@@ -1,7 +0,0 @@
import endPoints from 'widget/api/endPoints';
import { API } from 'widget/helpers/axios';
export const getMostReadArticles = async (slug, locale) => {
const urlData = endPoints.getMostReadArticles(slug, locale);
return API.get(urlData.url, { params: urlData.params });
};
-10
View File
@@ -97,15 +97,6 @@ const triggerCampaign = ({ websiteToken, campaignId, customAttributes }) => ({
},
});
const getMostReadArticles = (slug, locale) => ({
url: `/hc/${slug}/${locale}/articles.json`,
params: {
page: 1,
sort: 'views',
status: 1,
},
});
export default {
createConversation,
sendMessage,
@@ -115,5 +106,4 @@ export default {
getAvailableAgents,
getCampaigns,
triggerCampaign,
getMostReadArticles,
};
@@ -0,0 +1,74 @@
<script setup>
import { computed, onMounted } from 'vue';
import ArticleBlock from 'widget/components/pageComponents/Home/Article/ArticleBlock.vue';
import ArticleCardSkeletonLoader from 'widget/components/pageComponents/Home/Article/SkeletonLoader.vue';
import { useArticleStore } from 'widget/stores/articleStore';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useDarkMode } from 'widget/composables/useDarkMode';
const store = useArticleStore();
const router = useRouter();
const { prefersDarkMode } = useDarkMode();
const portal = computed(() => window.chatwootWebChannel.portal);
const locale = computed(() => {
const { locale: selectedLocale } = useI18n();
const {
allowed_locales: allowedLocales,
default_locale: defaultLocale = 'en',
} = portal.value.config;
// IMPORTANT: Variation strict locale matching, Follow iso_639_1_code
// If the exact match of a locale is available in the list of portal locales, return it
// Else return the default locale. Eg: `es` will not work if `es_ES` is available in the list
if (allowedLocales.includes(selectedLocale)) {
return locale;
}
return defaultLocale;
});
const fetchArticles = () => {
if (portal.value && !store.getRecords.length) {
store.index({ slug: portal.value.slug, locale: locale.value });
}
};
const openArticleInArticleViewer = link => {
let linkToOpen = `${link}?show_plain_layout=true`;
if (prefersDarkMode) {
linkToOpen = `${linkToOpen}&theme=dark`;
}
router.push({ name: 'article-viewer', query: { link: linkToOpen } });
};
const viewAllArticles = () => {
const {
portal: { slug },
} = window.chatwootWebChannel;
openArticleInArticleViewer(`/hc/${slug}/${locale.value}`);
};
const hasArticles = computed(
() => !store.getUIFlags.isFetching && !!store.getRecords.length
);
onMounted(() => fetchArticles());
</script>
<template>
<div
v-if="portal"
class="w-full shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2 px-5 py-4"
>
<ArticleBlock
v-if="hasArticles"
:articles="store.getRecords"
@view="openArticleInArticleViewer"
@view-all="viewAllArticles"
/>
<ArticleCardSkeletonLoader v-else />
</div>
<div v-else />
</template>
-2
View File
@@ -10,7 +10,6 @@ import events from 'widget/store/modules/events';
import globalConfig from 'shared/store/globalConfig';
import message from 'widget/store/modules/message';
import campaign from 'widget/store/modules/campaign';
import article from 'widget/store/modules/articles';
export default createStore({
modules: {
@@ -24,6 +23,5 @@ export default createStore({
globalConfig,
message,
campaign,
article,
},
});
@@ -1,55 +0,0 @@
import { getMostReadArticles } from 'widget/api/article';
const state = {
records: [],
uiFlags: {
isError: false,
hasFetched: false,
isFetching: false,
},
};
export const getters = {
uiFlags: $state => $state.uiFlags,
popularArticles: $state => $state.records,
};
export const actions = {
fetch: async ({ commit }, { slug, locale }) => {
commit('setIsFetching', true);
commit('setError', false);
try {
const { data } = await getMostReadArticles(slug, locale);
const { payload = [] } = data;
if (payload.length) {
commit('setArticles', payload);
}
} catch (error) {
commit('setError', true);
} finally {
commit('setIsFetching', false);
}
},
};
export const mutations = {
setArticles($state, data) {
$state.records = data;
},
setError($state, value) {
$state.uiFlags.isError = value;
},
setIsFetching($state, value) {
$state.uiFlags.isFetching = value;
},
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};
@@ -0,0 +1,13 @@
import { API } from 'widget/helpers/axios';
export default {
index: params => {
const url = `/hc/${params.slug}/${params.locale}/articles.json`;
const urlParams = {
page: 1,
sort: 'views',
status: 1,
};
return API.get(url, { params: urlParams });
},
};
@@ -0,0 +1,6 @@
import articleAPI from './api/articleAPI';
import { createResourceStore } from './piniaStoreFactory';
export const useArticleStore = createResourceStore('articles', {
api: articleAPI,
});
@@ -0,0 +1,105 @@
import { defineStore } from 'pinia';
export const createResourceStore = (
storeName,
{ api, customGetters = {}, customActions = {} }
) => {
return defineStore(storeName, {
state: () => ({
records: [],
uiFlags: {
isFetching: false,
isCreating: false,
isUpdating: false,
isDeleting: false,
},
error: null,
}),
getters: {
getRecords: state => state.records,
getUIFlags: state => state.uiFlags,
getError: state => state.error,
...customGetters,
},
actions: {
async index(params = {}) {
this.uiFlags.isFetching = true;
this.error = null;
try {
const {
data: { payload = [] },
} = await api.index(params);
this.records = [...payload];
} catch (error) {
this.error = error;
throw error;
} finally {
this.uiFlags.isFetching = false;
}
},
async create(payload) {
this.uiFlags.isCreating = true;
this.error = null;
try {
const { data } = await api.post(payload);
this.records.push(data.payload);
return data.payload;
} catch (error) {
this.error = error;
throw error;
} finally {
this.uiFlags.isCreating = false;
}
},
async update(id, payload) {
this.uiFlags.isUpdating = true;
this.error = null;
try {
const { data } = await api.put(id, payload);
const index = this.records.findIndex(record => record.id === id);
if (index !== -1) {
this.records[index] = data.payload;
}
return data.payload;
} catch (error) {
this.error = error;
throw error;
} finally {
this.uiFlags.isUpdating = false;
}
},
async delete(id) {
this.uiFlags.isDeleting = true;
this.error = null;
try {
await api.delete(id);
this.records = this.records.filter(record => record.id !== id);
} catch (error) {
this.error = error;
throw error;
} finally {
this.uiFlags.isDeleting = false;
}
},
reset() {
this.records = [];
this.error = null;
Object.keys(this.uiFlags).forEach(key => {
this.uiFlags[key] = false;
});
},
...customActions,
},
});
};
+4 -73
View File
@@ -1,62 +1,26 @@
<script>
import TeamAvailability from 'widget/components/TeamAvailability.vue';
import ArticleHero from 'widget/components/pageComponents/Home/Article/ArticleBlock.vue';
import ArticleCardSkeletonLoader from 'widget/components/pageComponents/Home/Article/SkeletonLoader.vue';
import { mapGetters } from 'vuex';
import { useDarkMode } from 'widget/composables/useDarkMode';
import routerMixin from 'widget/mixins/routerMixin';
import configMixin from 'widget/mixins/configMixin';
import ArticleContainer from '../components/pageComponents/Home/Article/ArticleContainer.vue';
export default {
name: 'Home',
components: {
ArticleHero,
ArticleContainer,
TeamAvailability,
ArticleCardSkeletonLoader,
},
mixins: [configMixin, routerMixin],
setup() {
const { prefersDarkMode } = useDarkMode();
return { prefersDarkMode };
},
computed: {
...mapGetters({
availableAgents: 'agent/availableAgents',
conversationSize: 'conversation/getConversationSize',
unreadMessageCount: 'conversation/getUnreadMessageCount',
popularArticles: 'article/popularArticles',
articleUiFlags: 'article/uiFlags',
}),
widgetLocale() {
return this.$i18n.locale || 'en';
},
portal() {
return window.chatwootWebChannel.portal;
},
defaultLocale() {
const widgetLocale = this.widgetLocale;
const { allowed_locales: allowedLocales, default_locale: defaultLocale } =
this.portal.config;
},
// IMPORTANT: Variation strict locale matching, Follow iso_639_1_code
// If the exact match of a locale is available in the list of portal locales, return it
// Else return the default locale. Eg: `es` will not work if `es_ES` is available in the list
if (allowedLocales.includes(widgetLocale)) {
return widgetLocale;
}
return defaultLocale;
},
},
mounted() {
if (this.portal && this.popularArticles.length === 0) {
const locale = this.defaultLocale;
this.$store.dispatch('article/fetch', {
slug: this.portal.slug,
locale,
});
}
},
methods: {
startConversation() {
if (this.preChatFormEnabled && !this.conversationSize) {
@@ -64,24 +28,6 @@ export default {
}
return this.replaceRoute('messages');
},
openArticleInArticleViewer(link) {
let linkToOpen = `${link}?show_plain_layout=true`;
const isDark = this.prefersDarkMode;
if (isDark) {
linkToOpen = `${linkToOpen}&theme=dark`;
}
this.$router.push({
name: 'article-viewer',
query: { link: linkToOpen },
});
},
viewAllArticles() {
const locale = this.defaultLocale;
const {
portal: { slug },
} = window.chatwootWebChannel;
this.openArticleInArticleViewer(`/hc/${slug}/${locale}`);
},
},
};
</script>
@@ -95,21 +41,6 @@ export default {
@start-conversation="startConversation"
/>
<div
v-if="portal"
class="w-full shadow outline-1 outline outline-n-container rounded-xl bg-n-solid-2 px-5 py-4"
>
<ArticleHero
v-if="
!articleUiFlags.isFetching &&
!articleUiFlags.isError &&
popularArticles.length
"
:articles="popularArticles"
@view="openArticleInArticleViewer"
@view-all="viewAllArticles"
/>
<ArticleCardSkeletonLoader v-else />
</div>
<ArticleContainer />
</div>
</template>