feat: Adds the ability to edit article (#5232)
This commit is contained in:
parent
b5e497a6a2
commit
b71291619c
19 changed files with 326 additions and 130 deletions
|
@ -42,7 +42,8 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
def article_params
|
def article_params
|
||||||
params.require(:article).permit(
|
params.require(:article).permit(
|
||||||
:title, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status
|
:title, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title, :description,
|
||||||
|
{ tags: [] }]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,17 @@ class ArticlesAPI extends PortalsAPI {
|
||||||
if (category_slug) baseUrl += `&category_slug=${category_slug}`;
|
if (category_slug) baseUrl += `&category_slug=${category_slug}`;
|
||||||
return axios.get(baseUrl);
|
return axios.get(baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getArticle({ id, portalSlug }) {
|
||||||
|
return axios.get(`${this.url}/${portalSlug}/articles/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateArticle({ portalSlug, articleId, articleObj }) {
|
||||||
|
return axios.patch(
|
||||||
|
`${this.url}/${portalSlug}/articles/${articleId}`,
|
||||||
|
articleObj
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ArticlesAPI();
|
export default new ArticlesAPI();
|
||||||
|
|
|
@ -26,4 +26,30 @@ describe('#PortalAPI', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describeWithAPIMock('API calls', context => {
|
||||||
|
it('#getArticle', () => {
|
||||||
|
articlesAPI.getArticle({
|
||||||
|
id: 1,
|
||||||
|
portalSlug: 'room-rental',
|
||||||
|
});
|
||||||
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/portals/room-rental/articles/1'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describeWithAPIMock('API calls', context => {
|
||||||
|
it('#updateArticle', () => {
|
||||||
|
articlesAPI.updateArticle({
|
||||||
|
articleId: 1,
|
||||||
|
portalSlug: 'room-rental',
|
||||||
|
articleObj: { title: 'Update shipping address' },
|
||||||
|
});
|
||||||
|
expect(context.axiosMock.patch).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/portals/room-rental/articles/1',
|
||||||
|
{
|
||||||
|
title: 'Update shipping address',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,13 +18,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EDIT_HEADER": {
|
"EDIT_HEADER": {
|
||||||
|
"ALL_ARTICLES": "All Articles",
|
||||||
"PUBLISH_BUTTON": "Publish",
|
"PUBLISH_BUTTON": "Publish",
|
||||||
"PREVIEW": "Preview",
|
"PREVIEW": "Preview",
|
||||||
"ADD_TRANSLATION": "Add translation",
|
"ADD_TRANSLATION": "Add translation",
|
||||||
"OPEN_SIDEBAR": "Open sidebar",
|
"OPEN_SIDEBAR": "Open sidebar",
|
||||||
"CLOSE_SIDEBAR": "Close sidebar",
|
"CLOSE_SIDEBAR": "Close sidebar",
|
||||||
"SAVING": "Draft saving...",
|
"SAVING": "Saving...",
|
||||||
"SAVED": "Draft saved"
|
"SAVED": "Saved"
|
||||||
},
|
},
|
||||||
"ARTICLE_SETTINGS": {
|
"ARTICLE_SETTINGS": {
|
||||||
"TITLE": "Article Settings",
|
"TITLE": "Article Settings",
|
||||||
|
@ -175,8 +176,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"EDIT_ARTICLE": {
|
"EDIT_ARTICLE": {
|
||||||
|
"LOADING": "Loading article...",
|
||||||
"TITLE_PLACEHOLDER": "Article title goes here",
|
"TITLE_PLACEHOLDER": "Article title goes here",
|
||||||
"CONTENT_PLACEHOLDER": "Write your article here"
|
"CONTENT_PLACEHOLDER": "Write your article here",
|
||||||
|
"API": {
|
||||||
|
"ERROR": "Error while saving article"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"SIDEBAR": {
|
"SIDEBAR": {
|
||||||
"SEARCH": {
|
"SEARCH": {
|
||||||
|
|
|
@ -25,7 +25,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { debounce } from '@chatwoot/utils';
|
||||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootMessageEditor,
|
WootMessageEditor,
|
||||||
|
@ -49,6 +51,13 @@ export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
this.articleTitle = this.article.title;
|
this.articleTitle = this.article.title;
|
||||||
this.articleContent = this.article.content;
|
this.articleContent = this.article.content;
|
||||||
|
this.saveArticle = debounce(
|
||||||
|
values => {
|
||||||
|
this.$emit('save-article', values);
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
false
|
||||||
|
);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onFocus() {
|
onFocus() {
|
||||||
|
@ -58,10 +67,10 @@ export default {
|
||||||
this.$emit('blur');
|
this.$emit('blur');
|
||||||
},
|
},
|
||||||
onTitleInput() {
|
onTitleInput() {
|
||||||
this.$emit('titleInput', this.articleTitle);
|
this.saveArticle({ title: this.articleTitle });
|
||||||
},
|
},
|
||||||
onContentInput() {
|
onContentInput() {
|
||||||
this.$emit('contentInput', this.articleContent);
|
this.saveArticle({ content: this.articleContent });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
|
@ -6,6 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right--wrap">
|
<div class="header-right--wrap">
|
||||||
<woot-button
|
<woot-button
|
||||||
|
v-if="shouldShowSettings"
|
||||||
class-names="article--buttons"
|
class-names="article--buttons"
|
||||||
icon="filter"
|
icon="filter"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
|
@ -16,6 +17,7 @@
|
||||||
{{ $t('HELP_CENTER.HEADER.FILTER') }}
|
{{ $t('HELP_CENTER.HEADER.FILTER') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
<woot-button
|
<woot-button
|
||||||
|
v-if="shouldShowSettings"
|
||||||
class-names="article--buttons"
|
class-names="article--buttons"
|
||||||
icon="arrow-sort"
|
icon="arrow-sort"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
|
@ -68,6 +70,7 @@
|
||||||
</woot-dropdown-menu>
|
</woot-dropdown-menu>
|
||||||
</div>
|
</div>
|
||||||
<woot-button
|
<woot-button
|
||||||
|
v-if="shouldShowSettings"
|
||||||
v-tooltip.top-end="$t('HELP_CENTER.HEADER.SETTINGS_BUTTON')"
|
v-tooltip.top-end="$t('HELP_CENTER.HEADER.SETTINGS_BUTTON')"
|
||||||
icon="settings"
|
icon="settings"
|
||||||
class-names="article--buttons"
|
class-names="article--buttons"
|
||||||
|
@ -113,6 +116,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
shouldShowSettings: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -12,9 +12,10 @@
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right--wrap">
|
<div class="header-right--wrap">
|
||||||
<span v-if="showDraftStatus" class="draft-status">
|
<span v-if="isUpdating || isSaved" class="draft-status">
|
||||||
{{ draftStatusText }}
|
{{ statusText }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<woot-button
|
<woot-button
|
||||||
class-names="article--buttons"
|
class-names="article--buttons"
|
||||||
icon="globe"
|
icon="globe"
|
||||||
|
@ -73,9 +74,13 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
draftState: {
|
isUpdating: {
|
||||||
type: String,
|
type: Boolean,
|
||||||
default: '',
|
default: false,
|
||||||
|
},
|
||||||
|
isSaved: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -84,20 +89,10 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isDraftStatusSavingOrSaved() {
|
statusText() {
|
||||||
return this.draftState === 'saving' || 'saved';
|
return this.isUpdating
|
||||||
},
|
? this.$t('HELP_CENTER.EDIT_HEADER.SAVING')
|
||||||
draftStatusText() {
|
: this.$t('HELP_CENTER.EDIT_HEADER.SAVED');
|
||||||
if (this.draftState === 'saving') {
|
|
||||||
return this.$t('HELP_CENTER.EDIT_HEADER.SAVING');
|
|
||||||
}
|
|
||||||
if (this.draftState === 'saved') {
|
|
||||||
return this.$t('HELP_CENTER.EDIT_HEADER.SAVED');
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
showDraftStatus() {
|
|
||||||
return this.isDraftStatusSavingOrSaved;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -150,5 +145,14 @@ export default {
|
||||||
color: var(--s-400);
|
color: var(--s-400);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: var(--font-size-mini);
|
font-size: var(--font-size-mini);
|
||||||
|
animation: fadeIn 1s;
|
||||||
|
@keyframes fadeIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -186,6 +186,7 @@ export default {
|
||||||
portalSlug: this.selectedPortalSlug,
|
portalSlug: this.selectedPortalSlug,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
this.$store.dispatch('agents/get');
|
||||||
},
|
},
|
||||||
toggleKeyShortcutModal() {
|
toggleKeyShortcutModal() {
|
||||||
this.showShortcutModal = true;
|
this.showShortcutModal = true;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import EditArticle from './EditArticle.vue';
|
import ArticleEditor from './ArticleEditor.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Components/Help Center',
|
title: 'Components/Help Center',
|
||||||
component: EditArticle,
|
component: ArticleEditor,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
article: {
|
article: {
|
||||||
defaultValue: {},
|
defaultValue: {},
|
||||||
|
@ -16,9 +16,9 @@ export default {
|
||||||
|
|
||||||
const Template = (args, { argTypes }) => ({
|
const Template = (args, { argTypes }) => ({
|
||||||
props: Object.keys(argTypes),
|
props: Object.keys(argTypes),
|
||||||
components: { EditArticle },
|
components: { ArticleEditor },
|
||||||
template:
|
template:
|
||||||
'<edit-article v-bind="$props" @focus="onFocus" @blur="onBlur"></edit-article>',
|
'<article-editor v-bind="$props" @focus="onFocus" @blur="onBlur"></-article>',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EditArticleView = Template.bind({});
|
export const EditArticleView = Template.bind({});
|
|
@ -8,7 +8,7 @@
|
||||||
<label>
|
<label>
|
||||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.LABEL') }}
|
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.LABEL') }}
|
||||||
<multiselect-dropdown
|
<multiselect-dropdown
|
||||||
:options="categoryList"
|
:options="categories"
|
||||||
:selected-item="selectedCategory"
|
:selected-item="selectedCategory"
|
||||||
:has-thumbnail="false"
|
:has-thumbnail="false"
|
||||||
:multiselector-title="
|
:multiselector-title="
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
<label>
|
<label>
|
||||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.LABEL') }}
|
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.LABEL') }}
|
||||||
<multiselect-dropdown
|
<multiselect-dropdown
|
||||||
:options="authorList"
|
:options="agents"
|
||||||
:selected-item="assignedAuthor"
|
:selected-item="assignedAuthor"
|
||||||
:multiselector-title="
|
:multiselector-title="
|
||||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.TITLE')
|
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.TITLE')
|
||||||
|
@ -51,18 +51,19 @@
|
||||||
<label>
|
<label>
|
||||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TITLE.LABEL') }}
|
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TITLE.LABEL') }}
|
||||||
<textarea
|
<textarea
|
||||||
v-model="title"
|
v-model="metaTitle"
|
||||||
rows="3"
|
rows="3"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TITLE.PLACEHOLDER')
|
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TITLE.PLACEHOLDER')
|
||||||
"
|
"
|
||||||
|
@input="onChangeMetaInput"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_DESCRIPTION.LABEL') }}
|
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_DESCRIPTION.LABEL') }}
|
||||||
<textarea
|
<textarea
|
||||||
v-model="description"
|
v-model="metaDescription"
|
||||||
rows="3"
|
rows="3"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
|
@ -70,19 +71,20 @@
|
||||||
'HELP_CENTER.ARTICLE_SETTINGS.FORM.META_DESCRIPTION.PLACEHOLDER'
|
'HELP_CENTER.ARTICLE_SETTINGS.FORM.META_DESCRIPTION.PLACEHOLDER'
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
@input="onChangeMetaInput"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TAGS.LABEL') }}
|
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TAGS.LABEL') }}
|
||||||
<multiselect
|
<multiselect
|
||||||
ref="tagInput"
|
ref="tagInput"
|
||||||
v-model="values"
|
v-model="metaTags"
|
||||||
:placeholder="
|
:placeholder="
|
||||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TAGS.PLACEHOLDER')
|
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TAGS.PLACEHOLDER')
|
||||||
"
|
"
|
||||||
label="name"
|
label="name"
|
||||||
|
:options="metaOptions"
|
||||||
track-by="name"
|
track-by="name"
|
||||||
:options="options"
|
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:taggable="true"
|
:taggable="true"
|
||||||
@tag="addTagValue"
|
@tag="addTagValue"
|
||||||
|
@ -115,60 +117,88 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown';
|
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { debounce } from '@chatwoot/utils';
|
||||||
|
import { isEmptyObject } from 'dashboard/helper/commons.js';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
MultiselectDropdown,
|
MultiselectDropdown,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
article: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// Dummy value
|
metaTitle: '',
|
||||||
categoryList: [
|
metaDescription: '',
|
||||||
{
|
metaTags: [],
|
||||||
id: 1,
|
metaOptions: [],
|
||||||
name: 'Getting started',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Features',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selectedCategory: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Features',
|
|
||||||
},
|
|
||||||
authorList: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'John Doe',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Jane Doe',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
assignedAuthor: {
|
|
||||||
id: 1,
|
|
||||||
name: 'John Doe',
|
|
||||||
},
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
values: [],
|
|
||||||
options: [],
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
categories: 'categories/allCategories',
|
||||||
|
agents: 'agents/getAgents',
|
||||||
|
}),
|
||||||
|
assignedAuthor() {
|
||||||
|
return this.article?.author;
|
||||||
|
},
|
||||||
|
selectedCategory() {
|
||||||
|
return this.article?.category;
|
||||||
|
},
|
||||||
|
allTags() {
|
||||||
|
return this.metaTags.map(item => item.name);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (!isEmptyObject(this.article.meta)) {
|
||||||
|
const {
|
||||||
|
meta: { title = '', description = '', tags = [] },
|
||||||
|
} = this.article;
|
||||||
|
this.metaTitle = title;
|
||||||
|
this.metaDescription = description;
|
||||||
|
this.metaTags = this.formattedTags({ tags });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveArticle = debounce(
|
||||||
|
() => {
|
||||||
|
this.$emit('save-article', {
|
||||||
|
meta: {
|
||||||
|
title: this.metaTitle,
|
||||||
|
description: this.metaDescription,
|
||||||
|
tags: this.allTags,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
1000,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formattedTags({ tags }) {
|
||||||
|
return tags.map(tag => ({
|
||||||
|
name: tag,
|
||||||
|
}));
|
||||||
|
},
|
||||||
addTagValue(tagValue) {
|
addTagValue(tagValue) {
|
||||||
const tag = {
|
const tag = {
|
||||||
name: tagValue,
|
name: tagValue,
|
||||||
};
|
};
|
||||||
this.values.push(tag);
|
this.metaTags.push(tag);
|
||||||
this.$refs.tagInput.$el.focus();
|
this.$refs.tagInput.$el.focus();
|
||||||
|
this.saveArticle();
|
||||||
},
|
},
|
||||||
onClickSelectCategory() {
|
onClickSelectCategory({ id }) {
|
||||||
this.$emit('select-category');
|
this.$emit('save-article', { category_id: id });
|
||||||
},
|
},
|
||||||
onClickAssignAuthor() {
|
onClickAssignAuthor({ id }) {
|
||||||
this.$emit('assign-author');
|
this.$emit('save-article', { author_id: id });
|
||||||
|
},
|
||||||
|
onChangeMetaInput() {
|
||||||
|
this.saveArticle();
|
||||||
},
|
},
|
||||||
onClickArchiveArticle() {
|
onClickArchiveArticle() {
|
||||||
this.$emit('archive-article');
|
this.$emit('archive-article');
|
||||||
|
|
|
@ -1,39 +1,131 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="article-container">
|
||||||
<edit-article-header
|
<div
|
||||||
back-button-label="All Articles"
|
class="edit-article--container"
|
||||||
draft-state="saved"
|
:class="{ 'is-sidebar-open': showArticleSettings }"
|
||||||
@back="onClickGoBack"
|
>
|
||||||
|
<edit-article-header
|
||||||
|
:back-button-label="$t('HELP_CENTER.HEADER.TITLES.ALL_ARTICLES')"
|
||||||
|
:is-updating="isUpdating"
|
||||||
|
:is-saved="isSaved"
|
||||||
|
@back="onClickGoBack"
|
||||||
|
@open="openArticleSettings"
|
||||||
|
@close="closeArticleSettings"
|
||||||
|
/>
|
||||||
|
<div v-if="isFetching" class="text-center p-normal fs-default h-full">
|
||||||
|
<spinner size="" />
|
||||||
|
<span>{{ $t('HELP_CENTER.EDIT_ARTICLE.LOADING') }}</span>
|
||||||
|
</div>
|
||||||
|
<article-editor
|
||||||
|
v-else
|
||||||
|
:is-settings-sidebar-open="showArticleSettings"
|
||||||
|
:article="article"
|
||||||
|
@save-article="saveArticle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<article-settings
|
||||||
|
v-if="showArticleSettings"
|
||||||
|
:article="article"
|
||||||
|
@save-article="saveArticle"
|
||||||
/>
|
/>
|
||||||
<edit-article-field :article="article" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import EditArticleHeader from 'dashboard/routes/dashboard/helpcenter/components/Header/EditArticleHeader';
|
import { mapGetters } from 'vuex';
|
||||||
import EditArticleField from 'dashboard/components/helpCenter/EditArticle';
|
import EditArticleHeader from '../../components/Header/EditArticleHeader.vue';
|
||||||
|
import ArticleEditor from '../../components/ArticleEditor.vue';
|
||||||
|
import ArticleSettings from './ArticleSettings.vue';
|
||||||
|
import Spinner from 'shared/components/Spinner';
|
||||||
|
import portalMixin from '../../mixins/portalMixin';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditArticleHeader,
|
EditArticleHeader,
|
||||||
EditArticleField,
|
ArticleEditor,
|
||||||
|
Spinner,
|
||||||
|
ArticleSettings,
|
||||||
},
|
},
|
||||||
props: {
|
mixins: [portalMixin, alertMixin],
|
||||||
article: {
|
data() {
|
||||||
type: Object,
|
return {
|
||||||
default: () => {},
|
isUpdating: false,
|
||||||
|
isSaved: false,
|
||||||
|
showArticleSettings: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
isFetching: 'articles/isFetching',
|
||||||
|
articles: 'articles/articles',
|
||||||
|
}),
|
||||||
|
article() {
|
||||||
|
return this.$store.getters['articles/articleById'](this.articleId);
|
||||||
},
|
},
|
||||||
|
articleId() {
|
||||||
|
return this.$route.params.articleSlug;
|
||||||
|
},
|
||||||
|
selectedPortalSlug() {
|
||||||
|
return this.portalSlug || this.selectedPortal?.slug;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchArticleDetails();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onClickGoBack() {
|
onClickGoBack() {
|
||||||
this.$router.push({ name: 'list_all_locale_articles' });
|
this.$router.push({ name: 'list_all_locale_articles' });
|
||||||
},
|
},
|
||||||
|
fetchArticleDetails() {
|
||||||
|
this.$store.dispatch('articles/show', {
|
||||||
|
id: this.articleId,
|
||||||
|
portalSlug: this.selectedPortalSlug,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async saveArticle({ ...values }) {
|
||||||
|
this.isUpdating = true;
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('articles/update', {
|
||||||
|
portalSlug: this.selectedPortalSlug,
|
||||||
|
articleId: this.articleId,
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.alertMessage =
|
||||||
|
error?.message ||
|
||||||
|
this.$t('HELP_CENTER.EDIT_ARTICLE.API.ERROR_MESSAGE');
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isUpdating = false;
|
||||||
|
this.isSaved = true;
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openArticleSettings() {
|
||||||
|
this.showArticleSettings = true;
|
||||||
|
},
|
||||||
|
closeArticleSettings() {
|
||||||
|
this.showArticleSettings = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.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;
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
|
.edit-article--container {
|
||||||
|
flex: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-sidebar-open {
|
||||||
|
flex: 0.7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,35 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="article-container">
|
<div class="container">
|
||||||
<div
|
<edit-article-header
|
||||||
class="edit-article--container"
|
back-button-label="All Articles"
|
||||||
:class="{ 'is-sidebar-open': showArticleSettings }"
|
draft-state="saved"
|
||||||
>
|
@back="onClickGoBack"
|
||||||
<edit-article-header
|
/>
|
||||||
back-button-label="All Articles"
|
<article-editor @titleInput="titleInput" @contentInput="contentInput" />
|
||||||
draft-state="saved"
|
|
||||||
@back="onClickGoBack"
|
|
||||||
@open="openArticleSettings"
|
|
||||||
@close="closeArticleSettings"
|
|
||||||
/>
|
|
||||||
<edit-article-field
|
|
||||||
:is-settings-sidebar-open="showArticleSettings"
|
|
||||||
@titleInput="titleInput"
|
|
||||||
@contentInput="contentInput"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<article-settings v-if="showArticleSettings" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import EditArticleHeader from 'dashboard/routes/dashboard/helpcenter/components/Header/EditArticleHeader';
|
import EditArticleHeader from 'dashboard/routes/dashboard/helpcenter/components/Header/EditArticleHeader';
|
||||||
import EditArticleField from 'dashboard/components/helpCenter/EditArticle';
|
import ArticleEditor from '../../components/ArticleEditor.vue';
|
||||||
import ArticleSettings from 'dashboard/routes/dashboard/helpcenter/pages/articles/ArticleSettings';
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditArticleHeader,
|
EditArticleHeader,
|
||||||
EditArticleField,
|
ArticleEditor,
|
||||||
ArticleSettings,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -62,18 +62,24 @@ export const actions = {
|
||||||
commit(types.SET_UI_FLAG, { isFetching: false });
|
commit(types.SET_UI_FLAG, { isFetching: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update: async ({ commit }, params) => {
|
update: async ({ commit }, { portalSlug, articleId, ...articleObj }) => {
|
||||||
const articleId = params.id;
|
|
||||||
commit(types.ADD_ARTICLE_FLAG, {
|
commit(types.ADD_ARTICLE_FLAG, {
|
||||||
uiFlags: {
|
uiFlags: {
|
||||||
isUpdating: true,
|
isUpdating: true,
|
||||||
},
|
},
|
||||||
articleId,
|
articleId,
|
||||||
});
|
});
|
||||||
try {
|
|
||||||
const { data } = await articlesAPI.update(params);
|
|
||||||
|
|
||||||
commit(types.UPDATE_ARTICLE, data);
|
try {
|
||||||
|
const {
|
||||||
|
data: { payload },
|
||||||
|
} = await articlesAPI.updateArticle({
|
||||||
|
portalSlug,
|
||||||
|
articleId,
|
||||||
|
articleObj,
|
||||||
|
});
|
||||||
|
|
||||||
|
commit(types.UPDATE_ARTICLE, payload);
|
||||||
|
|
||||||
return articleId;
|
return articleId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const mutations = {
|
||||||
[types.CLEAR_ARTICLES]: $state => {
|
[types.CLEAR_ARTICLES]: $state => {
|
||||||
Vue.set($state.articles, 'byId', {});
|
Vue.set($state.articles, 'byId', {});
|
||||||
Vue.set($state.articles, 'allIds', []);
|
Vue.set($state.articles, 'allIds', []);
|
||||||
Vue.set($state.articles, 'uiFlags', {});
|
Vue.set($state.articles, 'uiFlags.byId', {});
|
||||||
},
|
},
|
||||||
[types.ADD_MANY_ARTICLES]($state, articles) {
|
[types.ADD_MANY_ARTICLES]($state, articles) {
|
||||||
const allArticles = { ...$state.articles.byId };
|
const allArticles = { ...$state.articles.byId };
|
||||||
|
@ -55,7 +55,6 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
[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;
|
||||||
|
|
||||||
Vue.set($state.articles.byId, articleId, {
|
Vue.set($state.articles.byId, articleId, {
|
||||||
|
|
|
@ -89,8 +89,15 @@ describe('#actions', () => {
|
||||||
|
|
||||||
describe('#update', () => {
|
describe('#update', () => {
|
||||||
it('sends correct actions if API is success', async () => {
|
it('sends correct actions if API is success', async () => {
|
||||||
axios.patch.mockResolvedValue({ data: articleList[0] });
|
axios.patch.mockResolvedValue({ data: { payload: articleList[0] } });
|
||||||
await actions.update({ commit }, articleList[0]);
|
await actions.update(
|
||||||
|
{ commit },
|
||||||
|
{
|
||||||
|
portalSlug: 'room-rental',
|
||||||
|
articleId: 1,
|
||||||
|
title: 'Documents are required to complete KYC',
|
||||||
|
}
|
||||||
|
);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
types.default.ADD_ARTICLE_FLAG,
|
types.default.ADD_ARTICLE_FLAG,
|
||||||
|
@ -105,9 +112,17 @@ describe('#actions', () => {
|
||||||
});
|
});
|
||||||
it('sends correct actions if API is error', async () => {
|
it('sends correct actions if API is error', async () => {
|
||||||
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
await expect(actions.update({ commit }, articleList[0])).rejects.toThrow(
|
await expect(
|
||||||
Error
|
actions.update(
|
||||||
);
|
{ commit },
|
||||||
|
{
|
||||||
|
portalSlug: 'room-rental',
|
||||||
|
articleId: 1,
|
||||||
|
title: 'Documents are required to complete KYC',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).rejects.toThrow(Error);
|
||||||
|
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
types.default.ADD_ARTICLE_FLAG,
|
types.default.ADD_ARTICLE_FLAG,
|
||||||
|
|
|
@ -106,7 +106,11 @@ describe('#mutations', () => {
|
||||||
mutations[types.CLEAR_ARTICLES](state);
|
mutations[types.CLEAR_ARTICLES](state);
|
||||||
expect(state.articles.allIds).toEqual([]);
|
expect(state.articles.allIds).toEqual([]);
|
||||||
expect(state.articles.byId).toEqual({});
|
expect(state.articles.byId).toEqual({});
|
||||||
expect(state.articles.uiFlags).toEqual({});
|
expect(state.articles.uiFlags).toEqual({
|
||||||
|
byId: {
|
||||||
|
'1': { isFetching: false, isUpdating: true, isDeleting: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@ json.description article.description
|
||||||
json.status article.status
|
json.status article.status
|
||||||
json.account_id article.account_id
|
json.account_id article.account_id
|
||||||
json.updated_at article.updated_at.to_i
|
json.updated_at article.updated_at.to_i
|
||||||
|
json.meta article.meta
|
||||||
json.category do
|
json.category do
|
||||||
json.id article.category_id
|
json.id article.category_id
|
||||||
json.name article.category.name
|
json.name article.category.name
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braid/vue-formulate": "^2.5.2",
|
"@braid/vue-formulate": "^2.5.2",
|
||||||
"@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517",
|
"@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517",
|
||||||
"@chatwoot/utils": "^0.0.6",
|
"@chatwoot/utils": "^0.0.10",
|
||||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||||
"@rails/actioncable": "6.1.3",
|
"@rails/actioncable": "6.1.3",
|
||||||
"@rails/webpacker": "5.3.0",
|
"@rails/webpacker": "5.3.0",
|
||||||
|
|
|
@ -1406,10 +1406,10 @@
|
||||||
prosemirror-state "^1.3.3"
|
prosemirror-state "^1.3.3"
|
||||||
prosemirror-view "^1.17.2"
|
prosemirror-view "^1.17.2"
|
||||||
|
|
||||||
"@chatwoot/utils@^0.0.6":
|
"@chatwoot/utils@^0.0.10":
|
||||||
version "0.0.6"
|
version "0.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/@chatwoot/utils/-/utils-0.0.6.tgz#76d7b17d692b5b656c565b9b714b98e0f2bc1324"
|
resolved "https://registry.npmjs.org/@chatwoot/utils/-/utils-0.0.10.tgz#59f68cc28d8718b261ebed8b9c94d2c493b6c67f"
|
||||||
integrity sha512-fCvULfJSFSylDAiGh1cPAX5nQkVsmG5ASGm/E6YBYg8cox/2JU179JFstdtTxrIJg/YeHukcaq85Gc+/16ShPQ==
|
integrity sha512-Zd+wQTblWKUV1mhcXoabcfoLygx/Ock5pP0JQdfqW64lubhjYaRR4gCutEgqUcQB4nuOUH7MZ7BTzdZm4RoM/g==
|
||||||
dependencies:
|
dependencies:
|
||||||
date-fns "^2.22.1"
|
date-fns "^2.22.1"
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue