mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	feat: Adds the ability to edit article (#5232)
This commit is contained in:
		| @@ -42,7 +42,8 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController | ||||
|  | ||||
|   def article_params | ||||
|     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 | ||||
|  | ||||
|   | ||||
| @@ -21,6 +21,17 @@ class ArticlesAPI extends PortalsAPI { | ||||
|     if (category_slug) baseUrl += `&category_slug=${category_slug}`; | ||||
|     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(); | ||||
|   | ||||
| @@ -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": { | ||||
|       "ALL_ARTICLES": "All Articles", | ||||
|       "PUBLISH_BUTTON": "Publish", | ||||
|       "PREVIEW": "Preview", | ||||
|       "ADD_TRANSLATION": "Add translation", | ||||
|       "OPEN_SIDEBAR": "Open sidebar", | ||||
|       "CLOSE_SIDEBAR": "Close sidebar", | ||||
|       "SAVING": "Draft saving...", | ||||
|       "SAVED": "Draft saved" | ||||
|       "SAVING": "Saving...", | ||||
|       "SAVED": "Saved" | ||||
|     }, | ||||
|     "ARTICLE_SETTINGS": { | ||||
|       "TITLE": "Article Settings", | ||||
| @@ -175,8 +176,12 @@ | ||||
|       } | ||||
|     }, | ||||
|     "EDIT_ARTICLE": { | ||||
|       "LOADING": "Loading article...", | ||||
|       "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": { | ||||
|       "SEARCH": { | ||||
|   | ||||
| @@ -25,7 +25,9 @@ | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import { debounce } from '@chatwoot/utils'; | ||||
| import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue'; | ||||
| 
 | ||||
| export default { | ||||
|   components: { | ||||
|     WootMessageEditor, | ||||
| @@ -49,6 +51,13 @@ export default { | ||||
|   mounted() { | ||||
|     this.articleTitle = this.article.title; | ||||
|     this.articleContent = this.article.content; | ||||
|     this.saveArticle = debounce( | ||||
|       values => { | ||||
|         this.$emit('save-article', values); | ||||
|       }, | ||||
|       300, | ||||
|       false | ||||
|     ); | ||||
|   }, | ||||
|   methods: { | ||||
|     onFocus() { | ||||
| @@ -58,10 +67,10 @@ export default { | ||||
|       this.$emit('blur'); | ||||
|     }, | ||||
|     onTitleInput() { | ||||
|       this.$emit('titleInput', this.articleTitle); | ||||
|       this.saveArticle({ title: this.articleTitle }); | ||||
|     }, | ||||
|     onContentInput() { | ||||
|       this.$emit('contentInput', this.articleContent); | ||||
|       this.saveArticle({ content: this.articleContent }); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| @@ -6,6 +6,7 @@ | ||||
|     </div> | ||||
|     <div class="header-right--wrap"> | ||||
|       <woot-button | ||||
|         v-if="shouldShowSettings" | ||||
|         class-names="article--buttons" | ||||
|         icon="filter" | ||||
|         color-scheme="secondary" | ||||
| @@ -16,6 +17,7 @@ | ||||
|         {{ $t('HELP_CENTER.HEADER.FILTER') }} | ||||
|       </woot-button> | ||||
|       <woot-button | ||||
|         v-if="shouldShowSettings" | ||||
|         class-names="article--buttons" | ||||
|         icon="arrow-sort" | ||||
|         color-scheme="secondary" | ||||
| @@ -68,6 +70,7 @@ | ||||
|         </woot-dropdown-menu> | ||||
|       </div> | ||||
|       <woot-button | ||||
|         v-if="shouldShowSettings" | ||||
|         v-tooltip.top-end="$t('HELP_CENTER.HEADER.SETTINGS_BUTTON')" | ||||
|         icon="settings" | ||||
|         class-names="article--buttons" | ||||
| @@ -113,6 +116,10 @@ export default { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     shouldShowSettings: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|   | ||||
| @@ -12,9 +12,10 @@ | ||||
|       </woot-button> | ||||
|     </div> | ||||
|     <div class="header-right--wrap"> | ||||
|       <span v-if="showDraftStatus" class="draft-status"> | ||||
|         {{ draftStatusText }} | ||||
|       <span v-if="isUpdating || isSaved" class="draft-status"> | ||||
|         {{ statusText }} | ||||
|       </span> | ||||
|  | ||||
|       <woot-button | ||||
|         class-names="article--buttons" | ||||
|         icon="globe" | ||||
| @@ -73,9 +74,13 @@ export default { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     draftState: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     isUpdating: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|     isSaved: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
| @@ -84,20 +89,10 @@ export default { | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     isDraftStatusSavingOrSaved() { | ||||
|       return this.draftState === 'saving' || 'saved'; | ||||
|     }, | ||||
|     draftStatusText() { | ||||
|       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; | ||||
|     statusText() { | ||||
|       return this.isUpdating | ||||
|         ? this.$t('HELP_CENTER.EDIT_HEADER.SAVING') | ||||
|         : this.$t('HELP_CENTER.EDIT_HEADER.SAVED'); | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
| @@ -150,5 +145,14 @@ export default { | ||||
|   color: var(--s-400); | ||||
|   align-items: center; | ||||
|   font-size: var(--font-size-mini); | ||||
|   animation: fadeIn 1s; | ||||
|   @keyframes fadeIn { | ||||
|     0% { | ||||
|       opacity: 0; | ||||
|     } | ||||
|     100% { | ||||
|       opacity: 1; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -186,6 +186,7 @@ export default { | ||||
|           portalSlug: this.selectedPortalSlug, | ||||
|         }); | ||||
|       }); | ||||
|       this.$store.dispatch('agents/get'); | ||||
|     }, | ||||
|     toggleKeyShortcutModal() { | ||||
|       this.showShortcutModal = true; | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { action } from '@storybook/addon-actions'; | ||||
| import EditArticle from './EditArticle.vue'; | ||||
| import ArticleEditor from './ArticleEditor.vue'; | ||||
| 
 | ||||
| export default { | ||||
|   title: 'Components/Help Center', | ||||
|   component: EditArticle, | ||||
|   component: ArticleEditor, | ||||
|   argTypes: { | ||||
|     article: { | ||||
|       defaultValue: {}, | ||||
| @@ -16,9 +16,9 @@ export default { | ||||
| 
 | ||||
| const Template = (args, { argTypes }) => ({ | ||||
|   props: Object.keys(argTypes), | ||||
|   components: { EditArticle }, | ||||
|   components: { ArticleEditor }, | ||||
|   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({}); | ||||
| @@ -8,7 +8,7 @@ | ||||
|         <label> | ||||
|           {{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.LABEL') }} | ||||
|           <multiselect-dropdown | ||||
|             :options="categoryList" | ||||
|             :options="categories" | ||||
|             :selected-item="selectedCategory" | ||||
|             :has-thumbnail="false" | ||||
|             :multiselector-title=" | ||||
| @@ -31,7 +31,7 @@ | ||||
|         <label> | ||||
|           {{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.LABEL') }} | ||||
|           <multiselect-dropdown | ||||
|             :options="authorList" | ||||
|             :options="agents" | ||||
|             :selected-item="assignedAuthor" | ||||
|             :multiselector-title=" | ||||
|               $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.TITLE') | ||||
| @@ -51,18 +51,19 @@ | ||||
|         <label> | ||||
|           {{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TITLE.LABEL') }} | ||||
|           <textarea | ||||
|             v-model="title" | ||||
|             v-model="metaTitle" | ||||
|             rows="3" | ||||
|             type="text" | ||||
|             :placeholder=" | ||||
|               $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TITLE.PLACEHOLDER') | ||||
|             " | ||||
|             @input="onChangeMetaInput" | ||||
|           /> | ||||
|         </label> | ||||
|         <label> | ||||
|           {{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_DESCRIPTION.LABEL') }} | ||||
|           <textarea | ||||
|             v-model="description" | ||||
|             v-model="metaDescription" | ||||
|             rows="3" | ||||
|             type="text" | ||||
|             :placeholder=" | ||||
| @@ -70,19 +71,20 @@ | ||||
|                 'HELP_CENTER.ARTICLE_SETTINGS.FORM.META_DESCRIPTION.PLACEHOLDER' | ||||
|               ) | ||||
|             " | ||||
|             @input="onChangeMetaInput" | ||||
|           /> | ||||
|         </label> | ||||
|         <label> | ||||
|           {{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TAGS.LABEL') }} | ||||
|           <multiselect | ||||
|             ref="tagInput" | ||||
|             v-model="values" | ||||
|             v-model="metaTags" | ||||
|             :placeholder=" | ||||
|               $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TAGS.PLACEHOLDER') | ||||
|             " | ||||
|             label="name" | ||||
|             :options="metaOptions" | ||||
|             track-by="name" | ||||
|             :options="options" | ||||
|             :multiple="true" | ||||
|             :taggable="true" | ||||
|             @tag="addTagValue" | ||||
| @@ -115,60 +117,88 @@ | ||||
|  | ||||
| <script> | ||||
| 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 { | ||||
|   components: { | ||||
|     MultiselectDropdown, | ||||
|   }, | ||||
|   props: { | ||||
|     article: { | ||||
|       type: Object, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       // Dummy value | ||||
|       categoryList: [ | ||||
|         { | ||||
|           id: 1, | ||||
|           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: [], | ||||
|       metaTitle: '', | ||||
|       metaDescription: '', | ||||
|       metaTags: [], | ||||
|       metaOptions: [], | ||||
|     }; | ||||
|   }, | ||||
|   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: { | ||||
|     formattedTags({ tags }) { | ||||
|       return tags.map(tag => ({ | ||||
|         name: tag, | ||||
|       })); | ||||
|     }, | ||||
|     addTagValue(tagValue) { | ||||
|       const tag = { | ||||
|         name: tagValue, | ||||
|       }; | ||||
|       this.values.push(tag); | ||||
|       this.metaTags.push(tag); | ||||
|       this.$refs.tagInput.$el.focus(); | ||||
|       this.saveArticle(); | ||||
|     }, | ||||
|     onClickSelectCategory() { | ||||
|       this.$emit('select-category'); | ||||
|     onClickSelectCategory({ id }) { | ||||
|       this.$emit('save-article', { category_id: id }); | ||||
|     }, | ||||
|     onClickAssignAuthor() { | ||||
|       this.$emit('assign-author'); | ||||
|     onClickAssignAuthor({ id }) { | ||||
|       this.$emit('save-article', { author_id: id }); | ||||
|     }, | ||||
|     onChangeMetaInput() { | ||||
|       this.saveArticle(); | ||||
|     }, | ||||
|     onClickArchiveArticle() { | ||||
|       this.$emit('archive-article'); | ||||
|   | ||||
| @@ -1,39 +1,131 @@ | ||||
| <template> | ||||
|   <div class="container"> | ||||
|     <edit-article-header | ||||
|       back-button-label="All Articles" | ||||
|       draft-state="saved" | ||||
|       @back="onClickGoBack" | ||||
|   <div class="article-container"> | ||||
|     <div | ||||
|       class="edit-article--container" | ||||
|       :class="{ 'is-sidebar-open': showArticleSettings }" | ||||
|     > | ||||
|       <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> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import EditArticleHeader from 'dashboard/routes/dashboard/helpcenter/components/Header/EditArticleHeader'; | ||||
| import EditArticleField from 'dashboard/components/helpCenter/EditArticle'; | ||||
| import { mapGetters } from 'vuex'; | ||||
| 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 { | ||||
|   components: { | ||||
|     EditArticleHeader, | ||||
|     EditArticleField, | ||||
|     ArticleEditor, | ||||
|     Spinner, | ||||
|     ArticleSettings, | ||||
|   }, | ||||
|   props: { | ||||
|     article: { | ||||
|       type: Object, | ||||
|       default: () => {}, | ||||
|   mixins: [portalMixin, alertMixin], | ||||
|   data() { | ||||
|     return { | ||||
|       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: { | ||||
|     onClickGoBack() { | ||||
|       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> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .container { | ||||
| .article-container { | ||||
|   display: flex; | ||||
|   padding: var(--space-small) var(--space-normal); | ||||
|   width: 100%; | ||||
|   flex: 1; | ||||
|   overflow: scroll; | ||||
|  | ||||
|   .edit-article--container { | ||||
|     flex: 1; | ||||
|     flex-shrink: 0; | ||||
|     overflow: scroll; | ||||
|   } | ||||
|  | ||||
|   .is-sidebar-open { | ||||
|     flex: 0.7; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,35 +1,21 @@ | ||||
| <template> | ||||
|   <div class="article-container"> | ||||
|     <div | ||||
|       class="edit-article--container" | ||||
|       :class="{ 'is-sidebar-open': showArticleSettings }" | ||||
|     > | ||||
|       <edit-article-header | ||||
|         back-button-label="All Articles" | ||||
|         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 class="container"> | ||||
|     <edit-article-header | ||||
|       back-button-label="All Articles" | ||||
|       draft-state="saved" | ||||
|       @back="onClickGoBack" | ||||
|     /> | ||||
|     <article-editor @titleInput="titleInput" @contentInput="contentInput" /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import EditArticleHeader from 'dashboard/routes/dashboard/helpcenter/components/Header/EditArticleHeader'; | ||||
| import EditArticleField from 'dashboard/components/helpCenter/EditArticle'; | ||||
| import ArticleSettings from 'dashboard/routes/dashboard/helpcenter/pages/articles/ArticleSettings'; | ||||
| import ArticleEditor from '../../components/ArticleEditor.vue'; | ||||
| export default { | ||||
|   components: { | ||||
|     EditArticleHeader, | ||||
|     EditArticleField, | ||||
|     ArticleSettings, | ||||
|     ArticleEditor, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|   | ||||
| @@ -62,18 +62,24 @@ export const actions = { | ||||
|       commit(types.SET_UI_FLAG, { isFetching: false }); | ||||
|     } | ||||
|   }, | ||||
|   update: async ({ commit }, params) => { | ||||
|     const articleId = params.id; | ||||
|   update: async ({ commit }, { portalSlug, articleId, ...articleObj }) => { | ||||
|     commit(types.ADD_ARTICLE_FLAG, { | ||||
|       uiFlags: { | ||||
|         isUpdating: true, | ||||
|       }, | ||||
|       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; | ||||
|     } catch (error) { | ||||
|   | ||||
| @@ -19,7 +19,7 @@ export const mutations = { | ||||
|   [types.CLEAR_ARTICLES]: $state => { | ||||
|     Vue.set($state.articles, 'byId', {}); | ||||
|     Vue.set($state.articles, 'allIds', []); | ||||
|     Vue.set($state.articles, 'uiFlags', {}); | ||||
|     Vue.set($state.articles, 'uiFlags.byId', {}); | ||||
|   }, | ||||
|   [types.ADD_MANY_ARTICLES]($state, articles) { | ||||
|     const allArticles = { ...$state.articles.byId }; | ||||
| @@ -55,7 +55,6 @@ export const mutations = { | ||||
|   }, | ||||
|   [types.UPDATE_ARTICLE]($state, article) { | ||||
|     const articleId = article.id; | ||||
|  | ||||
|     if (!$state.articles.allIds.includes(articleId)) return; | ||||
|  | ||||
|     Vue.set($state.articles.byId, articleId, { | ||||
|   | ||||
| @@ -89,8 +89,15 @@ describe('#actions', () => { | ||||
|  | ||||
|   describe('#update', () => { | ||||
|     it('sends correct actions if API is success', async () => { | ||||
|       axios.patch.mockResolvedValue({ data: articleList[0] }); | ||||
|       await actions.update({ commit }, articleList[0]); | ||||
|       axios.patch.mockResolvedValue({ data: { payload: articleList[0] } }); | ||||
|       await actions.update( | ||||
|         { commit }, | ||||
|         { | ||||
|           portalSlug: 'room-rental', | ||||
|           articleId: 1, | ||||
|           title: 'Documents are required to complete KYC', | ||||
|         } | ||||
|       ); | ||||
|       expect(commit.mock.calls).toEqual([ | ||||
|         [ | ||||
|           types.default.ADD_ARTICLE_FLAG, | ||||
| @@ -105,9 +112,17 @@ describe('#actions', () => { | ||||
|     }); | ||||
|     it('sends correct actions if API is error', async () => { | ||||
|       axios.patch.mockRejectedValue({ message: 'Incorrect header' }); | ||||
|       await expect(actions.update({ commit }, articleList[0])).rejects.toThrow( | ||||
|         Error | ||||
|       ); | ||||
|       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.ADD_ARTICLE_FLAG, | ||||
|   | ||||
| @@ -106,7 +106,11 @@ describe('#mutations', () => { | ||||
|       mutations[types.CLEAR_ARTICLES](state); | ||||
|       expect(state.articles.allIds).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.account_id article.account_id | ||||
| json.updated_at article.updated_at.to_i | ||||
|  | ||||
| json.meta article.meta | ||||
| json.category do | ||||
|   json.id article.category_id | ||||
|   json.name article.category.name | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|   "dependencies": { | ||||
|     "@braid/vue-formulate": "^2.5.2", | ||||
|     "@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", | ||||
|     "@rails/actioncable": "6.1.3", | ||||
|     "@rails/webpacker": "5.3.0", | ||||
|   | ||||
| @@ -1406,10 +1406,10 @@ | ||||
|     prosemirror-state "^1.3.3" | ||||
|     prosemirror-view "^1.17.2" | ||||
|  | ||||
| "@chatwoot/utils@^0.0.6": | ||||
|   version "0.0.6" | ||||
|   resolved "https://registry.yarnpkg.com/@chatwoot/utils/-/utils-0.0.6.tgz#76d7b17d692b5b656c565b9b714b98e0f2bc1324" | ||||
|   integrity sha512-fCvULfJSFSylDAiGh1cPAX5nQkVsmG5ASGm/E6YBYg8cox/2JU179JFstdtTxrIJg/YeHukcaq85Gc+/16ShPQ== | ||||
| "@chatwoot/utils@^0.0.10": | ||||
|   version "0.0.10" | ||||
|   resolved "https://registry.npmjs.org/@chatwoot/utils/-/utils-0.0.10.tgz#59f68cc28d8718b261ebed8b9c94d2c493b6c67f" | ||||
|   integrity sha512-Zd+wQTblWKUV1mhcXoabcfoLygx/Ock5pP0JQdfqW64lubhjYaRR4gCutEgqUcQB4nuOUH7MZ7BTzdZm4RoM/g== | ||||
|   dependencies: | ||||
|     date-fns "^2.22.1" | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Muhsin Keloth
					Muhsin Keloth