feat: Adds the ability to create a new article (#5255)
This commit is contained in:
parent
45d0d101b1
commit
0cd08065d1
9 changed files with 117 additions and 36 deletions
|
@ -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();
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 },
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -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);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue