Migrate widgetPortalArticlesToPinia
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"md5": "^2.3.0",
|
||||
"mitt": "^3.0.1",
|
||||
"opus-recorder": "^8.0.5",
|
||||
"pinia": "^2.3.0",
|
||||
"semver": "7.6.3",
|
||||
"snakecase-keys": "^8.0.1",
|
||||
"timezone-phone-codes": "^0.0.2",
|
||||
|
||||
Generated
+10
-8
@@ -157,6 +157,9 @@ importers:
|
||||
opus-recorder:
|
||||
specifier: ^8.0.5
|
||||
version: 8.0.5
|
||||
pinia:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0(typescript@5.6.2)(vue@3.5.12(typescript@5.6.2))
|
||||
semver:
|
||||
specifier: 7.6.3
|
||||
version: 7.6.3
|
||||
@@ -3927,15 +3930,12 @@ packages:
|
||||
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
pinia@2.2.6:
|
||||
resolution: {integrity: sha512-vIsR8JkDN5Ga2vAxqOE2cJj4VtsHnzpR1Fz30kClxlh0yCHfec6uoMeM3e/ddqmwFUejK3NlrcQa/shnpyT4hA==}
|
||||
pinia@2.3.0:
|
||||
resolution: {integrity: sha512-ohZj3jla0LL0OH5PlLTDMzqKiVw2XARmC1XYLdLWIPBMdhDW/123ZWr4zVAhtJm+aoSkFa13pYXskAvAscIkhQ==}
|
||||
peerDependencies:
|
||||
'@vue/composition-api': ^1.4.0
|
||||
typescript: '>=4.4.4'
|
||||
vue: ^2.6.14 || ^3.5.11
|
||||
vue: ^2.7.0 || ^3.5.11
|
||||
peerDependenciesMeta:
|
||||
'@vue/composition-api':
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
@@ -5262,7 +5262,7 @@ snapshots:
|
||||
clsx: 2.1.1
|
||||
date-fns: 3.6.0
|
||||
lucide-vue-next: 0.394.0(vue@3.5.12(typescript@5.6.2))
|
||||
pinia: 2.2.6(typescript@5.6.2)(vue@3.5.12(typescript@5.6.2))
|
||||
pinia: 2.3.0(typescript@5.6.2)(vue@3.5.12(typescript@5.6.2))
|
||||
radix-vue: 1.9.9(vue@3.5.12(typescript@5.6.2))
|
||||
secure-ls: 2.0.0
|
||||
tailwind-merge: 2.5.4
|
||||
@@ -9317,13 +9317,15 @@ snapshots:
|
||||
|
||||
pify@2.3.0: {}
|
||||
|
||||
pinia@2.2.6(typescript@5.6.2)(vue@3.5.12(typescript@5.6.2)):
|
||||
pinia@2.3.0(typescript@5.6.2)(vue@3.5.12(typescript@5.6.2)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.12(typescript@5.6.2)
|
||||
vue-demi: 0.14.10(vue@3.5.12(typescript@5.6.2))
|
||||
optionalDependencies:
|
||||
typescript: 5.6.2
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
||||
pirates@4.0.6: {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user