Files
chatwoot/app/javascript/dashboard/store/modules/helpCenterArticles/specs/action.spec.js
Sivin Varghese 615a0c69fe 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>
2025-01-21 13:50:01 +05:30

283 lines
8.5 KiB
JavaScript

import axios from 'axios';
import { uploadExternalImage, uploadFile } from 'dashboard/helper/uploadHelper';
import * as types from '../../../mutation-types';
import { actions } from '../actions';
vi.mock('dashboard/helper/uploadHelper');
const articleList = [
{
id: 1,
category_id: 1,
title: 'Documents are required to complete KYC',
},
];
const camelCasedArticle = {
id: 1,
categoryId: 1,
title: 'Documents are required to complete KYC',
};
const commit = vi.fn();
const dispatch = vi.fn();
global.axios = axios;
vi.mock('axios');
describe('#actions', () => {
describe('#index', () => {
it('sends correct actions if API is success', async () => {
axios.get.mockResolvedValue({
data: {
payload: articleList,
meta: {
current_page: '1',
articles_count: 5,
},
},
});
await actions.index(
{ commit },
{ pageNumber: 1, portalSlug: 'test', locale: 'en' }
);
expect(commit.mock.calls).toEqual([
[types.default.SET_UI_FLAG, { isFetching: true }],
[types.default.CLEAR_ARTICLES],
[
types.default.ADD_MANY_ARTICLES,
[
{
id: 1,
categoryId: 1,
title: 'Documents are required to complete KYC',
},
],
],
[
types.default.SET_ARTICLES_META,
{ currentPage: '1', articlesCount: 5 },
],
[types.default.ADD_MANY_ARTICLES_ID, [1]],
[types.default.SET_UI_FLAG, { isFetching: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.get.mockRejectedValue({ message: 'Incorrect header' });
await expect(
actions.index(
{ commit },
{ pageNumber: 1, portalSlug: 'test', locale: 'en' }
)
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.default.SET_UI_FLAG, { isFetching: true }],
[types.default.SET_UI_FLAG, { isFetching: false }],
]);
});
});
describe('#create', () => {
it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({ data: { payload: camelCasedArticle } });
await actions.create({ commit, dispatch }, camelCasedArticle);
expect(commit.mock.calls).toEqual([
[types.default.SET_UI_FLAG, { isCreating: true }],
[types.default.ADD_ARTICLE, camelCasedArticle],
[types.default.ADD_ARTICLE_ID, 1],
[types.default.ADD_ARTICLE_FLAG, 1],
[types.default.SET_UI_FLAG, { isCreating: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.post.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.create({ commit }, articleList[0])).rejects.toThrow(
Error
);
expect(commit.mock.calls).toEqual([
[types.default.SET_UI_FLAG, { isCreating: true }],
[types.default.SET_UI_FLAG, { isCreating: false }],
]);
});
});
describe('#update', () => {
it('sends correct actions if API is success', async () => {
axios.patch.mockResolvedValue({ data: { payload: camelCasedArticle } });
await actions.update(
{ commit },
{
portalSlug: 'room-rental',
articleId: 1,
title: 'Documents are required to complete KYC',
}
);
expect(commit.mock.calls).toEqual([
[
types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isUpdating: true }, articleId: 1 },
],
[types.default.UPDATE_ARTICLE, camelCasedArticle],
[
types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isUpdating: false }, articleId: 1 },
],
]);
});
it('sends correct actions if API is error', async () => {
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
await expect(
actions.update(
{ commit },
{
portalSlug: 'room-rental',
articleId: 1,
title: 'Documents are required to complete KYC',
}
)
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[
types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isUpdating: true }, articleId: 1 },
],
[
types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isUpdating: false }, articleId: 1 },
],
]);
});
});
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] });
await actions.delete(
{ commit },
{ portalSlug: 'test', articleId: articleList[0].id }
);
expect(commit.mock.calls).toEqual([
[
types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isDeleting: true }, articleId: 1 },
],
[types.default.REMOVE_ARTICLE, articleList[0].id],
[types.default.REMOVE_ARTICLE_ID, articleList[0].id],
[
types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isDeleting: false }, articleId: 1 },
],
]);
});
it('sends correct actions if API is error', async () => {
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
await expect(
actions.delete(
{ commit },
{ portalSlug: 'test', articleId: articleList[0].id }
)
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[
types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isDeleting: true }, articleId: 1 },
],
[
types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isDeleting: false }, articleId: 1 },
],
]);
});
});
describe('attachImage', () => {
it('should upload the file and return the fileUrl', async () => {
const mockFile = new Blob(['test'], { type: 'image/png' });
mockFile.name = 'test.png';
const mockFileUrl = 'https://test.com/test.png';
uploadFile.mockResolvedValueOnce({ fileUrl: mockFileUrl });
const result = await actions.attachImage({}, { file: mockFile });
expect(uploadFile).toHaveBeenCalledWith(mockFile);
expect(result).toBe(mockFileUrl);
});
it('should throw an error if the upload fails', async () => {
const mockFile = new Blob(['test'], { type: 'image/png' });
mockFile.name = 'test.png';
const mockError = new Error('Upload failed');
uploadFile.mockRejectedValueOnce(mockError);
await expect(actions.attachImage({}, { file: mockFile })).rejects.toThrow(
'Upload failed'
);
});
});
describe('uploadExternalImage', () => {
it('should upload the image from external URL and return the fileUrl', async () => {
const mockUrl = 'https://example.com/image.jpg';
const mockFileUrl = 'https://uploaded.example.com/image.jpg';
uploadExternalImage.mockResolvedValueOnce({ fileUrl: mockFileUrl });
// When
const result = await actions.uploadExternalImage({}, { url: mockUrl });
// Then
expect(uploadExternalImage).toHaveBeenCalledWith(mockUrl);
expect(result).toBe(mockFileUrl);
});
it('should throw an error if the upload fails', async () => {
const mockUrl = 'https://example.com/image.jpg';
const mockError = new Error('Upload failed');
uploadExternalImage.mockRejectedValueOnce(mockError);
await expect(
actions.uploadExternalImage({}, { url: mockUrl })
).rejects.toThrow('Upload failed');
});
});
});