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
);
}
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();

View file

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

View file

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

View file

@ -1,22 +1,27 @@
<template>
<div class="container">
<div class="article-container">
<edit-article-header
back-button-label="All Articles"
:back-button-label="$t('HELP_CENTER.HEADER.TITLES.ALL_ARTICLES')"
draft-state="saved"
@back="onClickGoBack"
@save-article="createNewArticle"
/>
<article-editor @titleInput="titleInput" @contentInput="contentInput" />
<article-editor :article="article" @save-article="createNewArticle" />
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import EditArticleHeader from 'dashboard/routes/dashboard/helpcenter/components/Header/EditArticleHeader';
import ArticleEditor from '../../components/ArticleEditor.vue';
import portalMixin from '../../mixins/portalMixin';
import alertMixin from 'shared/mixins/alertMixin.js';
export default {
components: {
EditArticleHeader,
ArticleEditor,
},
mixins: [portalMixin, alertMixin],
data() {
return {
articleTitle: '',
@ -24,21 +29,55 @@ export default {
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: {
onClickGoBack() {
this.$router.push({ name: 'list_all_locale_articles' });
},
titleInput(value) {
this.articleTitle = value;
async createNewArticle({ ...values }) {
const { title, content } = values;
if (title) this.articleTitle = title;
if (content) this.articleContent = content;
if (this.articleTitle && this.articleContent) {
try {
const articleId = await this.$store.dispatch('articles/create', {
portalSlug: this.selectedPortalSlug,
content: this.articleContent,
title: this.articleTitle,
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,
},
contentInput(value) {
this.articleContent = value;
},
openArticleSettings() {
this.showArticleSettings = true;
},
closeArticleSettings() {
this.showArticleSettings = false;
});
} 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>
.article-container {
display: flex;
padding: var(--space-small) var(--space-normal);
width: 100%;
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 });
try {
const { data } = await articlesAPI.create(params);
const { id: articleId } = data;
commit(types.ADD_ARTICLE, data);
const {
data: { payload },
} = 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_FLAG, articleId);
dispatch('portals/updatePortal', portal, { root: true });
return articleId;
} catch (error) {
return throwErrorMessage(error);
@ -63,7 +70,7 @@ export const actions = {
}
},
update: async ({ commit }, { portalSlug, articleId, ...articleObj }) => {
commit(types.ADD_ARTICLE_FLAG, {
commit(types.UPDATE_ARTICLE_FLAG, {
uiFlags: {
isUpdating: true,
},
@ -85,7 +92,7 @@ export const actions = {
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(types.ADD_ARTICLE_FLAG, {
commit(types.UPDATE_ARTICLE_FLAG, {
uiFlags: {
isUpdating: false,
},
@ -94,7 +101,7 @@ export const actions = {
}
},
delete: async ({ commit }, articleId) => {
commit(types.ADD_ARTICLE_FLAG, {
commit(types.UPDATE_ARTICLE_FLAG, {
uiFlags: {
isDeleting: true,
},
@ -109,7 +116,7 @@ export const actions = {
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(types.ADD_ARTICLE_FLAG, {
commit(types.UPDATE_ARTICLE_FLAG, {
uiFlags: {
isDeleting: false,
},

View file

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

View file

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

View file

@ -84,8 +84,10 @@ export const actions = {
});
}
},
setPortalId: async ({ commit }, 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_ID: 'ADD_MANY_ARTICLES_ID',
SET_ARTICLES_META: 'SET_ARTICLES_META',
UPDATE_ARTICLE_FLAG: 'UPDATE_ARTICLE_FLAG',
ADD_ARTICLE_FLAG: 'ADD_ARTICLE_FLAG',
UPDATE_ARTICLE: 'UPDATE_ARTICLE',
CLEAR_ARTICLES: 'CLEAR_ARTICLES',