mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +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 |   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" | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Muhsin Keloth
					Muhsin Keloth