feat: Adds the ability to create a new article (#5255)

This commit is contained in:
Muhsin Keloth 2022-08-18 11:45:08 +05:30 committed by GitHub
parent 45d0d101b1
commit 0cd08065d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 117 additions and 36 deletions

View file

@ -32,6 +32,16 @@ class ArticlesAPI extends PortalsAPI {
articleObj articleObj
); );
} }
createArticle({ portalSlug, articleObj }) {
const { content, title, author_id, category_id } = articleObj;
return axios.post(`${this.url}/${portalSlug}/articles`, {
content,
title,
author_id,
category_id,
});
}
} }
export default new ArticlesAPI(); export default new ArticlesAPI();

View file

@ -183,6 +183,9 @@
"ERROR": "Error while saving article" "ERROR": "Error while saving article"
} }
}, },
"CREATE_ARTICLE": {
"ERROR_MESSAGE": "Something went wrong. Please try again."
},
"SIDEBAR": { "SIDEBAR": {
"SEARCH": { "SEARCH": {
"PLACEHOLDER": "Search for articles" "PLACEHOLDER": "Search for articles"

View file

@ -51,12 +51,14 @@ export default {
isUpdating: false, isUpdating: false,
isSaved: false, isSaved: false,
showArticleSettings: false, showArticleSettings: false,
alertMessage: '',
}; };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
isFetching: 'articles/isFetching', isFetching: 'articles/isFetching',
articles: 'articles/articles', articles: 'articles/articles',
selectedPortal: 'portals/getSelectedPortal',
}), }),
article() { article() {
return this.$store.getters['articles/articleById'](this.articleId); return this.$store.getters['articles/articleById'](this.articleId);
@ -93,6 +95,7 @@ export default {
this.alertMessage = this.alertMessage =
error?.message || error?.message ||
this.$t('HELP_CENTER.EDIT_ARTICLE.API.ERROR_MESSAGE'); this.$t('HELP_CENTER.EDIT_ARTICLE.API.ERROR_MESSAGE');
this.showAlert(this.alertMessage);
} finally { } finally {
setTimeout(() => { setTimeout(() => {
this.isUpdating = false; this.isUpdating = false;

View file

@ -1,22 +1,27 @@
<template> <template>
<div class="container"> <div class="article-container">
<edit-article-header <edit-article-header
back-button-label="All Articles" :back-button-label="$t('HELP_CENTER.HEADER.TITLES.ALL_ARTICLES')"
draft-state="saved" draft-state="saved"
@back="onClickGoBack" @back="onClickGoBack"
@save-article="createNewArticle"
/> />
<article-editor @titleInput="titleInput" @contentInput="contentInput" /> <article-editor :article="article" @save-article="createNewArticle" />
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex';
import EditArticleHeader from 'dashboard/routes/dashboard/helpcenter/components/Header/EditArticleHeader'; import EditArticleHeader from 'dashboard/routes/dashboard/helpcenter/components/Header/EditArticleHeader';
import ArticleEditor from '../../components/ArticleEditor.vue'; import ArticleEditor from '../../components/ArticleEditor.vue';
import portalMixin from '../../mixins/portalMixin';
import alertMixin from 'shared/mixins/alertMixin.js';
export default { export default {
components: { components: {
EditArticleHeader, EditArticleHeader,
ArticleEditor, ArticleEditor,
}, },
mixins: [portalMixin, alertMixin],
data() { data() {
return { return {
articleTitle: '', articleTitle: '',
@ -24,21 +29,55 @@ export default {
showArticleSettings: false, showArticleSettings: false,
}; };
}, },
computed: {
...mapGetters({
selectedPortal: 'portals/getSelectedPortal',
currentUserID: 'getCurrentUserID',
categories: 'categories/allCategories',
}),
article() {
return { title: this.articleTitle, content: this.articleContent };
},
selectedPortalSlug() {
return this.portalSlug || this.selectedPortal?.slug;
},
categoryId() {
return this.categories.length ? this.categories[0].id : null;
},
},
methods: { methods: {
onClickGoBack() { onClickGoBack() {
this.$router.push({ name: 'list_all_locale_articles' }); this.$router.push({ name: 'list_all_locale_articles' });
}, },
titleInput(value) { async createNewArticle({ ...values }) {
this.articleTitle = value; const { title, content } = values;
}, if (title) this.articleTitle = title;
contentInput(value) { if (content) this.articleContent = content;
this.articleContent = value; if (this.articleTitle && this.articleContent) {
}, try {
openArticleSettings() { const articleId = await this.$store.dispatch('articles/create', {
this.showArticleSettings = true; portalSlug: this.selectedPortalSlug,
}, content: this.articleContent,
closeArticleSettings() { title: this.articleTitle,
this.showArticleSettings = false; author_id: this.currentUserID,
// TODO: Change to un categorized later when API supports
category_id: this.categoryId,
});
this.$router.push({
name: 'edit_article',
params: {
articleSlug: articleId,
portalSlug: this.selectedPortalSlug,
locale: this.locale,
},
});
} catch (error) {
this.alertMessage =
error?.message ||
this.$t('HELP_CENTER.CREATE_ARTICLE.API.ERROR_MESSAGE');
this.showAlert(this.alertMessage);
}
}
}, },
}, },
}; };
@ -46,7 +85,6 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.article-container { .article-container {
display: flex;
padding: var(--space-small) var(--space-normal); padding: var(--space-small) var(--space-normal);
width: 100%; width: 100%;
flex: 1; flex: 1;

View file

@ -32,13 +32,20 @@ export const actions = {
} }
}, },
create: async ({ commit }, params) => { create: async ({ commit, dispatch }, { portalSlug, ...articleObj }) => {
commit(types.SET_UI_FLAG, { isCreating: true }); commit(types.SET_UI_FLAG, { isCreating: true });
try { try {
const { data } = await articlesAPI.create(params); const {
const { id: articleId } = data; data: { payload },
commit(types.ADD_ARTICLE, data); } = await articlesAPI.createArticle({
portalSlug,
articleObj,
});
const { id: articleId, portal } = payload;
commit(types.ADD_ARTICLE, payload);
commit(types.ADD_ARTICLE_ID, articleId); commit(types.ADD_ARTICLE_ID, articleId);
commit(types.ADD_ARTICLE_FLAG, articleId);
dispatch('portals/updatePortal', portal, { root: true });
return articleId; return articleId;
} catch (error) { } catch (error) {
return throwErrorMessage(error); return throwErrorMessage(error);
@ -63,7 +70,7 @@ export const actions = {
} }
}, },
update: async ({ commit }, { portalSlug, articleId, ...articleObj }) => { update: async ({ commit }, { portalSlug, articleId, ...articleObj }) => {
commit(types.ADD_ARTICLE_FLAG, { commit(types.UPDATE_ARTICLE_FLAG, {
uiFlags: { uiFlags: {
isUpdating: true, isUpdating: true,
}, },
@ -85,7 +92,7 @@ export const actions = {
} catch (error) { } catch (error) {
return throwErrorMessage(error); return throwErrorMessage(error);
} finally { } finally {
commit(types.ADD_ARTICLE_FLAG, { commit(types.UPDATE_ARTICLE_FLAG, {
uiFlags: { uiFlags: {
isUpdating: false, isUpdating: false,
}, },
@ -94,7 +101,7 @@ export const actions = {
} }
}, },
delete: async ({ commit }, articleId) => { delete: async ({ commit }, articleId) => {
commit(types.ADD_ARTICLE_FLAG, { commit(types.UPDATE_ARTICLE_FLAG, {
uiFlags: { uiFlags: {
isDeleting: true, isDeleting: true,
}, },
@ -109,7 +116,7 @@ export const actions = {
} catch (error) { } catch (error) {
return throwErrorMessage(error); return throwErrorMessage(error);
} finally { } finally {
commit(types.ADD_ARTICLE_FLAG, { commit(types.UPDATE_ARTICLE_FLAG, {
uiFlags: { uiFlags: {
isDeleting: false, isDeleting: false,
}, },

View file

@ -26,6 +26,7 @@ export const mutations = {
articles.forEach(article => { articles.forEach(article => {
allArticles[article.id] = article; allArticles[article.id] = article;
}); });
Vue.set($state.articles, 'byId', allArticles); Vue.set($state.articles, 'byId', allArticles);
}, },
[types.ADD_MANY_ARTICLES_ID]($state, articleIds) { [types.ADD_MANY_ARTICLES_ID]($state, articleIds) {
@ -39,10 +40,13 @@ export const mutations = {
}, },
[types.ADD_ARTICLE_ID]: ($state, articleId) => { [types.ADD_ARTICLE_ID]: ($state, articleId) => {
if ($state.articles.allIds.includes(articleId)) return;
$state.articles.allIds.push(articleId); $state.articles.allIds.push(articleId);
}, },
[types.ADD_ARTICLE_FLAG]: ($state, { articleId, uiFlags }) => { [types.UPDATE_ARTICLE_FLAG]: ($state, { articleId, uiFlags }) => {
const flags = $state.articles.uiFlags.byId[articleId]; const flags =
Object.keys($state.articles.uiFlags.byId).includes(articleId) || {};
Vue.set($state.articles.uiFlags.byId, articleId, { Vue.set($state.articles.uiFlags.byId, articleId, {
...{ ...{
isFetching: false, isFetching: false,
@ -53,6 +57,16 @@ export const mutations = {
...uiFlags, ...uiFlags,
}); });
}, },
[types.ADD_ARTICLE_FLAG]: ($state, { articleId, uiFlags }) => {
Vue.set($state.articles.uiFlags.byId, articleId, {
...{
isFetching: false,
isUpdating: false,
isDeleting: false,
},
...uiFlags,
});
},
[types.UPDATE_ARTICLE]($state, article) { [types.UPDATE_ARTICLE]($state, article) {
const articleId = article.id; const articleId = article.id;
if (!$state.articles.allIds.includes(articleId)) return; if (!$state.articles.allIds.includes(articleId)) return;

View file

@ -9,6 +9,7 @@ const articleList = [
}, },
]; ];
const commit = jest.fn(); const commit = jest.fn();
const dispatch = jest.fn();
global.axios = axios; global.axios = axios;
jest.mock('axios'); jest.mock('axios');
@ -66,15 +67,17 @@ describe('#actions', () => {
describe('#create', () => { describe('#create', () => {
it('sends correct actions if API is success', async () => { it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({ data: articleList[0] }); axios.post.mockResolvedValue({ data: { payload: articleList[0] } });
await actions.create({ commit }, articleList[0]); await actions.create({ commit, dispatch }, articleList[0]);
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
[types.default.SET_UI_FLAG, { isCreating: true }], [types.default.SET_UI_FLAG, { isCreating: true }],
[types.default.ADD_ARTICLE, articleList[0]], [types.default.ADD_ARTICLE, articleList[0]],
[types.default.ADD_ARTICLE_ID, 1], [types.default.ADD_ARTICLE_ID, 1],
[types.default.ADD_ARTICLE_FLAG, 1],
[types.default.SET_UI_FLAG, { isCreating: false }], [types.default.SET_UI_FLAG, { isCreating: false }],
]); ]);
}); });
it('sends correct actions if API is error', async () => { it('sends correct actions if API is error', async () => {
axios.post.mockRejectedValue({ message: 'Incorrect header' }); axios.post.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.create({ commit }, articleList[0])).rejects.toThrow( await expect(actions.create({ commit }, articleList[0])).rejects.toThrow(
@ -100,12 +103,12 @@ describe('#actions', () => {
); );
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
[ [
types.default.ADD_ARTICLE_FLAG, types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isUpdating: true }, articleId: 1 }, { uiFlags: { isUpdating: true }, articleId: 1 },
], ],
[types.default.UPDATE_ARTICLE, articleList[0]], [types.default.UPDATE_ARTICLE, articleList[0]],
[ [
types.default.ADD_ARTICLE_FLAG, types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isUpdating: false }, articleId: 1 }, { uiFlags: { isUpdating: false }, articleId: 1 },
], ],
]); ]);
@ -125,11 +128,11 @@ describe('#actions', () => {
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
[ [
types.default.ADD_ARTICLE_FLAG, types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isUpdating: true }, articleId: 1 }, { uiFlags: { isUpdating: true }, articleId: 1 },
], ],
[ [
types.default.ADD_ARTICLE_FLAG, types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isUpdating: false }, articleId: 1 }, { uiFlags: { isUpdating: false }, articleId: 1 },
], ],
]); ]);
@ -142,13 +145,13 @@ describe('#actions', () => {
await actions.delete({ commit }, articleList[0].id); await actions.delete({ commit }, articleList[0].id);
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
[ [
types.default.ADD_ARTICLE_FLAG, types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isDeleting: true }, articleId: 1 }, { uiFlags: { isDeleting: true }, articleId: 1 },
], ],
[types.default.REMOVE_ARTICLE, articleList[0].id], [types.default.REMOVE_ARTICLE, articleList[0].id],
[types.default.REMOVE_ARTICLE_ID, articleList[0].id], [types.default.REMOVE_ARTICLE_ID, articleList[0].id],
[ [
types.default.ADD_ARTICLE_FLAG, types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isDeleting: false }, articleId: 1 }, { uiFlags: { isDeleting: false }, articleId: 1 },
], ],
]); ]);
@ -160,11 +163,11 @@ describe('#actions', () => {
).rejects.toThrow(Error); ).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
[ [
types.default.ADD_ARTICLE_FLAG, types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isDeleting: true }, articleId: 1 }, { uiFlags: { isDeleting: true }, articleId: 1 },
], ],
[ [
types.default.ADD_ARTICLE_FLAG, types.default.UPDATE_ARTICLE_FLAG,
{ uiFlags: { isDeleting: false }, articleId: 1 }, { uiFlags: { isDeleting: false }, articleId: 1 },
], ],
]); ]);

View file

@ -84,8 +84,10 @@ export const actions = {
}); });
} }
}, },
setPortalId: async ({ commit }, portalId) => { setPortalId: async ({ commit }, portalId) => {
commit(types.SET_SELECTED_PORTAL_ID, portalId); commit(types.SET_SELECTED_PORTAL_ID, portalId);
}, },
updatePortal: async ({ commit }, portal) => {
commit(types.UPDATE_PORTAL_ENTRY, portal);
},
}; };

View file

@ -227,6 +227,7 @@ export default {
ADD_MANY_ARTICLES: 'ADD_MANY_ARTICLES', ADD_MANY_ARTICLES: 'ADD_MANY_ARTICLES',
ADD_MANY_ARTICLES_ID: 'ADD_MANY_ARTICLES_ID', ADD_MANY_ARTICLES_ID: 'ADD_MANY_ARTICLES_ID',
SET_ARTICLES_META: 'SET_ARTICLES_META', SET_ARTICLES_META: 'SET_ARTICLES_META',
UPDATE_ARTICLE_FLAG: 'UPDATE_ARTICLE_FLAG',
ADD_ARTICLE_FLAG: 'ADD_ARTICLE_FLAG', ADD_ARTICLE_FLAG: 'ADD_ARTICLE_FLAG',
UPDATE_ARTICLE: 'UPDATE_ARTICLE', UPDATE_ARTICLE: 'UPDATE_ARTICLE',
CLEAR_ARTICLES: 'CLEAR_ARTICLES', CLEAR_ARTICLES: 'CLEAR_ARTICLES',