chore: Help center improvements (#10712)
# Pull Request Template ## Description Fixes https://linear.app/chatwoot/issue/CW-3913/issues-with-help-center **Fixes included** 1. > The default locale that is selected should the portal default locale. Now, we update the last active locale in UI settings after changing the selected locale from the article page header. This ensures that we see the last active locale-based categories on the category page and remember it when we return. Initially, it’s the default locale. 2. > I cannot switch to a different locale if there are no articles in the portal Now, the `v-if` condition that checked for the presence of articles in portal has been removed. Additionally, the locale length checks for the showing dropdown have been removed, allows locale switching even if article is not preset. 3. > Create or updating the article is quite painful, see the video Removed the `quickSave` and `saveAndSyncDebounced` usage for a newly creating article. 4. > I cannot see the articles if I delete the English locale (irrespective of what I choose as default locale) Now, the last active locale in UI settings will automatically update to the default locale when the last active locale is deleted. 5. > Set a new default locale other than `en` and delete the `en` locale preset in the portal. Then, adding a new locale will automatically set `en` as the default locale, even if the `en` locale not preset in the portal. Now, we pass default locale when we add a new locale. 6. Adds search for all dropdown menus 7. Update article count in realtime. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? **Check this linear issues** https://linear.app/chatwoot/issue/CW-3913/issues-with-help-center ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
@@ -36,6 +36,8 @@ const emit = defineEmits([
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const isNewArticle = computed(() => !props.article?.id);
|
||||
|
||||
const saveAndSync = value => {
|
||||
emit('saveArticle', value);
|
||||
};
|
||||
@@ -52,21 +54,32 @@ const quickSave = debounce(
|
||||
// 2.5 seconds is enough to know that the user has stopped typing and is taking a pause
|
||||
// so we can save the data to the backend and retrieve the updated data
|
||||
// this will update the local state with response data
|
||||
// Only use to save for existing articles
|
||||
const saveAndSyncDebounced = debounce(saveAndSync, 2500, false);
|
||||
|
||||
// Debounced save for new articles
|
||||
const quickSaveNewArticle = debounce(saveAndSync, 400, false);
|
||||
|
||||
const handleSave = value => {
|
||||
if (isNewArticle.value) {
|
||||
quickSaveNewArticle(value);
|
||||
} else {
|
||||
quickSave(value);
|
||||
saveAndSyncDebounced(value);
|
||||
}
|
||||
};
|
||||
|
||||
const articleTitle = computed({
|
||||
get: () => props.article.title,
|
||||
set: value => {
|
||||
quickSave({ title: value });
|
||||
saveAndSyncDebounced({ title: value });
|
||||
handleSave({ title: value });
|
||||
},
|
||||
});
|
||||
|
||||
const articleContent = computed({
|
||||
get: () => props.article.content,
|
||||
set: content => {
|
||||
quickSave({ content });
|
||||
saveAndSyncDebounced({ content });
|
||||
handleSave({ content });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -200,7 +200,8 @@ onMounted(() => {
|
||||
<DropdownMenu
|
||||
v-if="openAgentsList && hasAgentList"
|
||||
:menu-items="agentList"
|
||||
class="z-[100] w-48 mt-2 overflow-y-auto ltr:left-0 rtl:right-0 top-full max-h-52"
|
||||
show-search
|
||||
class="z-[100] w-48 mt-2 overflow-y-auto ltr:left-0 rtl:right-0 top-full max-h-60"
|
||||
@action="handleArticleAction"
|
||||
/>
|
||||
</OnClickOutside>
|
||||
@@ -231,7 +232,8 @@ onMounted(() => {
|
||||
<DropdownMenu
|
||||
v-if="openCategoryList && hasCategoryMenuItems"
|
||||
:menu-items="categoryList"
|
||||
class="w-48 mt-2 z-[100] overflow-y-auto left-0 top-full max-h-52"
|
||||
show-search
|
||||
class="w-48 mt-2 z-[100] overflow-y-auto left-0 top-full max-h-60"
|
||||
@action="handleArticleAction"
|
||||
/>
|
||||
</OnClickOutside>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { OnClickOutside } from '@vueuse/components';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import {
|
||||
ARTICLE_TABS,
|
||||
CATEGORY_ALL,
|
||||
@@ -37,6 +38,7 @@ const emit = defineEmits([
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const { updateUISettings } = useUISettings();
|
||||
|
||||
const isCategoryMenuOpen = ref(false);
|
||||
const isLocaleMenuOpen = ref(false);
|
||||
@@ -111,13 +113,12 @@ const localeMenuItems = computed(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
const hasMoreThanOneLocaleMenuItems = computed(() => {
|
||||
return localeMenuItems.value?.length > 1;
|
||||
});
|
||||
|
||||
const handleLocaleAction = ({ value }) => {
|
||||
emit('localeChange', value);
|
||||
isLocaleMenuOpen.value = false;
|
||||
updateUISettings({
|
||||
last_active_locale_code: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCategoryAction = ({ value }) => {
|
||||
@@ -143,7 +144,7 @@ const handleTabChange = value => {
|
||||
/>
|
||||
<div class="flex items-start justify-between w-full gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div v-if="hasMoreThanOneLocaleMenuItems" class="relative group">
|
||||
<div class="relative group">
|
||||
<OnClickOutside @trigger="isLocaleMenuOpen = false">
|
||||
<Button
|
||||
:label="activeLocaleName"
|
||||
@@ -157,6 +158,7 @@ const handleTabChange = value => {
|
||||
<DropdownMenu
|
||||
v-if="isLocaleMenuOpen"
|
||||
:menu-items="localeMenuItems"
|
||||
show-search
|
||||
class="left-0 w-40 max-w-[300px] mt-2 overflow-y-auto xl:right-0 top-full max-h-60"
|
||||
@action="handleLocaleAction"
|
||||
/>
|
||||
@@ -177,6 +179,7 @@ const handleTabChange = value => {
|
||||
<DropdownMenu
|
||||
v-if="isCategoryMenuOpen"
|
||||
:menu-items="categoryMenuItems"
|
||||
show-search
|
||||
class="left-0 w-48 mt-2 overflow-y-auto xl:right-0 top-full max-h-60"
|
||||
@action="handleCategoryAction"
|
||||
/>
|
||||
|
||||
@@ -99,11 +99,19 @@ const getStatusMessage = (status, isSuccess) => {
|
||||
: '';
|
||||
};
|
||||
|
||||
const updateMeta = () => {
|
||||
const updatePortalMeta = () => {
|
||||
const { portalSlug, locale } = route.params;
|
||||
return store.dispatch('portals/show', { portalSlug, locale });
|
||||
};
|
||||
|
||||
const updateArticlesMeta = () => {
|
||||
const { portalSlug, locale } = route.params;
|
||||
return store.dispatch('articles/updateArticleMeta', {
|
||||
portalSlug,
|
||||
locale,
|
||||
});
|
||||
};
|
||||
|
||||
const handleArticleAction = async (action, { status, id }) => {
|
||||
const { portalSlug } = route.params;
|
||||
try {
|
||||
@@ -127,7 +135,8 @@ const handleArticleAction = async (action, { status, id }) => {
|
||||
useTrack(PORTALS_EVENTS.PUBLISH_ARTICLE);
|
||||
}
|
||||
}
|
||||
await updateMeta();
|
||||
await updateArticlesMeta();
|
||||
await updatePortalMeta();
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.message ||
|
||||
|
||||
@@ -91,10 +91,7 @@ const articlesCount = computed(() => {
|
||||
});
|
||||
|
||||
const showArticleHeaderControls = computed(
|
||||
() =>
|
||||
!hasNoArticlesInPortal.value &&
|
||||
!props.isCategoryArticles &&
|
||||
!isSwitchingPortal.value
|
||||
() => !props.isCategoryArticles && !isSwitchingPortal.value
|
||||
);
|
||||
|
||||
const showCategoryHeaderControls = computed(
|
||||
|
||||
@@ -141,6 +141,7 @@ const handleBreadcrumbClick = () => {
|
||||
<DropdownMenu
|
||||
v-if="isLocaleMenuOpen"
|
||||
:menu-items="localeMenuItems"
|
||||
show-search
|
||||
class="left-0 w-40 mt-2 overflow-y-auto xl:right-0 top-full max-h-60"
|
||||
@action="handleLocaleAction"
|
||||
/>
|
||||
|
||||
@@ -49,8 +49,11 @@ const onCreate = async () => {
|
||||
|
||||
try {
|
||||
await store.dispatch('portals/update', {
|
||||
portalSlug: props.portal.slug,
|
||||
config: { allowed_locales: updatedLocales },
|
||||
portalSlug: props.portal?.slug,
|
||||
config: {
|
||||
allowed_locales: updatedLocales,
|
||||
default_locale: props.portal?.meta?.default_locale,
|
||||
},
|
||||
});
|
||||
|
||||
useTrack(PORTALS_EVENTS.CREATE_LOCALE, {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import LocaleCard from 'dashboard/components-next/HelpCenter/LocaleCard/LocaleCard.vue';
|
||||
import { useStore } from 'dashboard/composables/store';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
@@ -20,6 +21,7 @@ const props = defineProps({
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
const isLocaleDefault = code => {
|
||||
return props.portal?.meta?.default_locale === code;
|
||||
@@ -56,26 +58,40 @@ const changeDefaultLocale = ({ localeCode }) => {
|
||||
defaultLocale: localeCode,
|
||||
messageKey: 'CHANGE_DEFAULT_LOCALE',
|
||||
});
|
||||
|
||||
useTrack(PORTALS_EVENTS.SET_DEFAULT_LOCALE, {
|
||||
newLocale: localeCode,
|
||||
from: route.name,
|
||||
});
|
||||
};
|
||||
|
||||
const deletePortalLocale = ({ localeCode }) => {
|
||||
const updateLastActivePortal = async localeCode => {
|
||||
const { last_active_locale_code: lastActiveLocaleCode } =
|
||||
uiSettings.value || {};
|
||||
const defaultLocale = props.portal.meta.default_locale;
|
||||
|
||||
// Update UI settings only if deleting locale matches the last active locale in UI settings.
|
||||
if (localeCode === lastActiveLocaleCode) {
|
||||
await updateUISettings({
|
||||
last_active_locale_code: defaultLocale,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deletePortalLocale = async ({ localeCode }) => {
|
||||
const updatedLocales = props.locales
|
||||
.filter(locale => locale.code !== localeCode)
|
||||
.map(locale => locale.code);
|
||||
|
||||
const defaultLocale = props.portal.meta.default_locale;
|
||||
|
||||
updatePortalLocales({
|
||||
await updatePortalLocales({
|
||||
newAllowedLocales: updatedLocales,
|
||||
defaultLocale,
|
||||
messageKey: 'DELETE_LOCALE',
|
||||
});
|
||||
|
||||
await updateLastActivePortal(localeCode);
|
||||
|
||||
useTrack(PORTALS_EVENTS.DELETE_LOCALE, {
|
||||
deletedLocale: localeCode,
|
||||
from: route.name,
|
||||
|
||||
@@ -118,6 +118,21 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
updateArticleMeta: async ({ commit }, { portalSlug, locale }) => {
|
||||
try {
|
||||
const { data } = await articlesAPI.getArticles({
|
||||
pageNumber: 1,
|
||||
portalSlug,
|
||||
locale,
|
||||
});
|
||||
const meta = camelcaseKeys(data.meta);
|
||||
const { currentPage, ...metaWithoutCurrentPage } = meta;
|
||||
commit(types.SET_ARTICLES_META, metaWithoutCurrentPage);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ commit }, { portalSlug, articleId }) => {
|
||||
commit(types.UPDATE_ARTICLE_FLAG, {
|
||||
uiFlags: {
|
||||
|
||||
@@ -31,7 +31,10 @@ export const mutations = {
|
||||
},
|
||||
|
||||
[types.SET_ARTICLES_META]: ($state, meta) => {
|
||||
$state.meta = meta;
|
||||
$state.meta = {
|
||||
...$state.meta,
|
||||
...meta,
|
||||
};
|
||||
},
|
||||
|
||||
[types.ADD_ARTICLE_ID]: ($state, articleId) => {
|
||||
|
||||
@@ -150,6 +150,42 @@ describe('#actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateArticleMeta', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: {
|
||||
payload: articleList,
|
||||
meta: {
|
||||
all_articles_count: 56,
|
||||
archived_articles_count: 7,
|
||||
articles_count: 56,
|
||||
current_page: '1', // This is not needed, it cause pagination issues.
|
||||
draft_articles_count: 24,
|
||||
mine_articles_count: 44,
|
||||
published_count: 25,
|
||||
},
|
||||
},
|
||||
});
|
||||
await actions.updateArticleMeta(
|
||||
{ commit },
|
||||
{ pageNumber: 1, portalSlug: 'test', locale: 'en' }
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[
|
||||
types.default.SET_ARTICLES_META,
|
||||
{
|
||||
allArticlesCount: 56,
|
||||
archivedArticlesCount: 7,
|
||||
articlesCount: 56,
|
||||
draftArticlesCount: 24,
|
||||
mineArticlesCount: 44,
|
||||
publishedCount: 25,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.delete.mockResolvedValue({ data: articleList[0] });
|
||||
|
||||
@@ -47,6 +47,10 @@ describe('#mutations', () => {
|
||||
});
|
||||
|
||||
describe('#ARTICLES_META', () => {
|
||||
beforeEach(() => {
|
||||
state.meta = {};
|
||||
});
|
||||
|
||||
it('add meta to state', () => {
|
||||
mutations[types.SET_ARTICLES_META](state, {
|
||||
articles_count: 3,
|
||||
@@ -57,6 +61,31 @@ describe('#mutations', () => {
|
||||
current_page: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves existing meta values and updates only provided keys', () => {
|
||||
state.meta = {
|
||||
all_articles_count: 56,
|
||||
archived_articles_count: 5,
|
||||
articles_count: 56,
|
||||
current_page: '1',
|
||||
draft_articles_count: 26,
|
||||
published_count: 25,
|
||||
};
|
||||
|
||||
mutations[types.SET_ARTICLES_META](state, {
|
||||
articles_count: 3,
|
||||
draft_articles_count: 27,
|
||||
});
|
||||
|
||||
expect(state.meta).toEqual({
|
||||
all_articles_count: 56,
|
||||
archived_articles_count: 5,
|
||||
current_page: '1',
|
||||
articles_count: 3,
|
||||
draft_articles_count: 27,
|
||||
published_count: 25,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ADD_ARTICLE_ID', () => {
|
||||
|
||||
Reference in New Issue
Block a user