mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	feat: Add the ability add new portal (#5219)
This commit is contained in:
		| @@ -18,8 +18,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController | |||||||
|  |  | ||||||
|   def create |   def create | ||||||
|     @portal = Current.account.portals.build(portal_params) |     @portal = Current.account.portals.build(portal_params) | ||||||
|     render json: { error: @portal.errors.messages }, status: :unprocessable_entity and return unless @portal.valid? |  | ||||||
|  |  | ||||||
|     @portal.save! |     @portal.save! | ||||||
|     process_attached_logo |     process_attached_logo | ||||||
|   end |   end | ||||||
|   | |||||||
| @@ -75,3 +75,10 @@ export const convertToCategorySlug = text => { | |||||||
|     .replace(/[^\w ]+/g, '') |     .replace(/[^\w ]+/g, '') | ||||||
|     .replace(/ +/g, '-'); |     .replace(/ +/g, '-'); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const convertToPortalSlug = text => { | ||||||
|  |   return text | ||||||
|  |     .toLowerCase() | ||||||
|  |     .replace(/[^\w ]+/g, '') | ||||||
|  |     .replace(/ +/g, '-'); | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import { | |||||||
|   createPendingMessage, |   createPendingMessage, | ||||||
|   convertToAttributeSlug, |   convertToAttributeSlug, | ||||||
|   convertToCategorySlug, |   convertToCategorySlug, | ||||||
|  |   convertToPortalSlug, | ||||||
| } from '../commons'; | } from '../commons'; | ||||||
|  |  | ||||||
| describe('#getTypingUsersText', () => { | describe('#getTypingUsersText', () => { | ||||||
| @@ -104,3 +105,9 @@ describe('convertToCategorySlug', () => { | |||||||
|     ); |     ); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | describe('convertToPortalSlug', () => { | ||||||
|  |   it('should convert to slug', () => { | ||||||
|  |     expect(convertToPortalSlug('Room rental')).toBe('room-rental'); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|   | |||||||
| @@ -74,6 +74,54 @@ | |||||||
|             } |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  |       }, | ||||||
|  |       "ADD": { | ||||||
|  |         "TITLE": "Create a portal", | ||||||
|  |         "SUB_TITLE": "A Help Center in Chatwoot is known as a portal. You can have multiple portals and can have different locales for each portal.", | ||||||
|  |         "NAME": { | ||||||
|  |           "LABEL": "Name", | ||||||
|  |           "PLACEHOLDER": "Portal name", | ||||||
|  |           "HELP_TEXT": "The name will be used in the public facing portal internally", | ||||||
|  |           "ERROR": "Name is required" | ||||||
|  |         }, | ||||||
|  |         "PAGE_TITLE": { | ||||||
|  |           "LABEL": "Page Title", | ||||||
|  |           "PLACEHOLDER": "Portal page title", | ||||||
|  |           "HELP_TEXT": "The name will be used in the public facing portal", | ||||||
|  |           "ERROR": "Page title is required" | ||||||
|  |         }, | ||||||
|  |         "SLUG": { | ||||||
|  |           "LABEL": "Slug", | ||||||
|  |           "PLACEHOLDER": "Portal slug for urls", | ||||||
|  |           "HELP_TEXT": "app.chatwoot.com/portal/my-portal", | ||||||
|  |           "ERROR": "Slug is required" | ||||||
|  |         }, | ||||||
|  |         "DOMAIN": { | ||||||
|  |           "LABEL": "Custom Domain", | ||||||
|  |           "PLACEHOLDER": "Portal custom domain", | ||||||
|  |           "HELP_TEXT": "Add only If you want to use a custom domain for your portals", | ||||||
|  |           "ERROR": "Custom Domain is required" | ||||||
|  |         }, | ||||||
|  |         "HOME_PAGE_LINK": { | ||||||
|  |           "LABEL": "Home Page Link", | ||||||
|  |           "PLACEHOLDER": "Portal home page link", | ||||||
|  |           "HELP_TEXT": "The link used to return from the portal to the home page.", | ||||||
|  |           "ERROR": "Home Page Link is required" | ||||||
|  |         }, | ||||||
|  |         "HEADER_TEXT": { | ||||||
|  |           "LABEL": "Header Text", | ||||||
|  |           "PLACEHOLDER": "Portal header text", | ||||||
|  |           "HELP_TEXT": "Portal header text", | ||||||
|  |           "ERROR": "Portal header text is required" | ||||||
|  |         }, | ||||||
|  |         "BUTTONS": { | ||||||
|  |           "CREATE": "Create portal", | ||||||
|  |           "CANCEL": "Cancel" | ||||||
|  |         }, | ||||||
|  |         "API": { | ||||||
|  |           "SUCCESS_MESSAGE": "Portal created successfully.", | ||||||
|  |           "ERROR_MESSAGE": "Couldn't create the portal. Try again." | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "TABLE": { |     "TABLE": { | ||||||
|   | |||||||
| @@ -0,0 +1,185 @@ | |||||||
|  | <template> | ||||||
|  |   <modal :show.sync="show" :on-close="onClose"> | ||||||
|  |     <woot-modal-header | ||||||
|  |       :header-title="$t('HELP_CENTER.PORTAL.ADD.TITLE')" | ||||||
|  |       :header-content="$t('HELP_CENTER.PORTAL.ADD.SUB_TITLE')" | ||||||
|  |     /> | ||||||
|  |     <form class="row" @submit.prevent="onCreate"> | ||||||
|  |       <div class="medium-12 columns"> | ||||||
|  |         <woot-input | ||||||
|  |           v-model="name" | ||||||
|  |           :class="{ error: $v.name.$error }" | ||||||
|  |           class="medium-12 columns" | ||||||
|  |           :error="$v.name.$error ? $t('HELP_CENTER.PORTAL.ADD.NAME.ERROR') : ''" | ||||||
|  |           :label="$t('HELP_CENTER.PORTAL.ADD.NAME.LABEL')" | ||||||
|  |           :placeholder="$t('HELP_CENTER.PORTAL.ADD.NAME.PLACEHOLDER')" | ||||||
|  |           :help-text="$t('HELP_CENTER.PORTAL.ADD.NAME.HELP_TEXT')" | ||||||
|  |           @blur="$v.name.$touch" | ||||||
|  |           @input="onNameChange" | ||||||
|  |         /> | ||||||
|  |         <woot-input | ||||||
|  |           v-model="slug" | ||||||
|  |           :class="{ error: $v.slug.$error }" | ||||||
|  |           class="medium-12 columns" | ||||||
|  |           :error="$v.slug.$error ? $t('HELP_CENTER.PORTAL.ADD.SLUG.ERROR') : ''" | ||||||
|  |           :label="$t('HELP_CENTER.PORTAL.ADD.SLUG.LABEL')" | ||||||
|  |           :placeholder="$t('HELP_CENTER.PORTAL.ADD.SLUG.PLACEHOLDER')" | ||||||
|  |           :help-text="$t('HELP_CENTER.PORTAL.ADD.SLUG.HELP_TEXT')" | ||||||
|  |           @blur="$v.slug.$touch" | ||||||
|  |         /> | ||||||
|  |  | ||||||
|  |         <woot-input | ||||||
|  |           v-model="pageTitle" | ||||||
|  |           class="medium-12 columns" | ||||||
|  |           :label="$t('HELP_CENTER.PORTAL.ADD.PAGE_TITLE.LABEL')" | ||||||
|  |           :placeholder="$t('HELP_CENTER.PORTAL.ADD.PAGE_TITLE.PLACEHOLDER')" | ||||||
|  |           :help-text="$t('HELP_CENTER.PORTAL.ADD.PAGE_TITLE.HELP_TEXT')" | ||||||
|  |         /> | ||||||
|  |         <woot-input | ||||||
|  |           v-model="headerText" | ||||||
|  |           class="medium-12 columns" | ||||||
|  |           :label="$t('HELP_CENTER.PORTAL.ADD.HEADER_TEXT.LABEL')" | ||||||
|  |           :placeholder="$t('HELP_CENTER.PORTAL.ADD.HEADER_TEXT.PLACEHOLDER')" | ||||||
|  |           :help-text="$t('HELP_CENTER.PORTAL.ADD.HEADER_TEXT.HELP_TEXT')" | ||||||
|  |         /> | ||||||
|  |  | ||||||
|  |         <woot-input | ||||||
|  |           v-model="domain" | ||||||
|  |           class="medium-12 columns" | ||||||
|  |           :label="$t('HELP_CENTER.PORTAL.ADD.DOMAIN.LABEL')" | ||||||
|  |           :placeholder="$t('HELP_CENTER.PORTAL.ADD.DOMAIN.PLACEHOLDER')" | ||||||
|  |           :help-text="$t('HELP_CENTER.PORTAL.ADD.DOMAIN.HELP_TEXT')" | ||||||
|  |         /> | ||||||
|  |         <woot-input | ||||||
|  |           v-model="homePageLink" | ||||||
|  |           class="medium-12 columns" | ||||||
|  |           :label="$t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.LABEL')" | ||||||
|  |           :placeholder="$t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.PLACEHOLDER')" | ||||||
|  |           :help-text="$t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.HELP_TEXT')" | ||||||
|  |         /> | ||||||
|  |  | ||||||
|  |         <div class="medium-12 columns"> | ||||||
|  |           <div class="modal-footer justify-content-end w-full"> | ||||||
|  |             <woot-button | ||||||
|  |               class="button clear" | ||||||
|  |               :is-loading="uiFlags.isCreating" | ||||||
|  |               @click.prevent="onClose" | ||||||
|  |             > | ||||||
|  |               {{ $t('HELP_CENTER.PORTAL.ADD.BUTTONS.CANCEL') }} | ||||||
|  |             </woot-button> | ||||||
|  |             <woot-button> | ||||||
|  |               {{ $t('HELP_CENTER.PORTAL.ADD.BUTTONS.CREATE') }} | ||||||
|  |             </woot-button> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </form> | ||||||
|  |   </modal> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import { mapGetters } from 'vuex'; | ||||||
|  | import Modal from 'dashboard/components/Modal'; | ||||||
|  | import alertMixin from 'shared/mixins/alertMixin'; | ||||||
|  | import { required } from 'vuelidate/lib/validators'; | ||||||
|  | import { convertToPortalSlug } from 'dashboard/helper/commons.js'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { | ||||||
|  |     Modal, | ||||||
|  |   }, | ||||||
|  |   mixins: [alertMixin], | ||||||
|  |   props: { | ||||||
|  |     show: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: true, | ||||||
|  |     }, | ||||||
|  |     portalName: { | ||||||
|  |       type: String, | ||||||
|  |       default: '', | ||||||
|  |     }, | ||||||
|  |     locale: { | ||||||
|  |       type: String, | ||||||
|  |       default: '', | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       name: '', | ||||||
|  |       slug: '', | ||||||
|  |       domain: '', | ||||||
|  |       homePageLink: '', | ||||||
|  |       pageTitle: '', | ||||||
|  |       headerText: '', | ||||||
|  |       alertMessage: '', | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters({ | ||||||
|  |       uiFlags: 'portals/uiFlagsIn', | ||||||
|  |     }), | ||||||
|  |   }, | ||||||
|  |   validations: { | ||||||
|  |     name: { | ||||||
|  |       required, | ||||||
|  |     }, | ||||||
|  |     slug: { | ||||||
|  |       required, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     onNameChange() { | ||||||
|  |       this.slug = convertToPortalSlug(this.name); | ||||||
|  |     }, | ||||||
|  |     async onCreate() { | ||||||
|  |       this.$v.$touch(); | ||||||
|  |       if (this.$v.$invalid) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         await this.$store.dispatch('portals/create', { | ||||||
|  |           portal: { | ||||||
|  |             name: this.name, | ||||||
|  |             slug: this.slug, | ||||||
|  |             custom_domain: this.domain, | ||||||
|  |             // TODO: add support for choosing color | ||||||
|  |             color: '#1f93ff', | ||||||
|  |             homepage_link: this.homePageLink, | ||||||
|  |             page_title: this.pageTitle, | ||||||
|  |             header_text: this.headerText, | ||||||
|  |             config: { | ||||||
|  |               // TODO: add support for choosing locale | ||||||
|  |               allowed_locales: ['en'], | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }); | ||||||
|  |         this.alertMessage = this.$t( | ||||||
|  |           'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE' | ||||||
|  |         ); | ||||||
|  |         this.$emit('cancel'); | ||||||
|  |       } catch (error) { | ||||||
|  |         this.alertMessage = | ||||||
|  |           error?.message || this.$t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE'); | ||||||
|  |       } finally { | ||||||
|  |         this.showAlert(this.alertMessage); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     onClose() { | ||||||
|  |       this.$emit('cancel'); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | <style scoped lang="scss"> | ||||||
|  | .input-container::v-deep { | ||||||
|  |   margin: 0 0 var(--space-normal); | ||||||
|  |  | ||||||
|  |   input { | ||||||
|  |     margin-bottom: 0; | ||||||
|  |   } | ||||||
|  |   .message { | ||||||
|  |     margin-top: 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -2,7 +2,7 @@ | |||||||
|   <div class="container"> |   <div class="container"> | ||||||
|     <div class="header-wrap"> |     <div class="header-wrap"> | ||||||
|       <h1 class="page-title">{{ $t('HELP_CENTER.PORTAL.HEADER') }}</h1> |       <h1 class="page-title">{{ $t('HELP_CENTER.PORTAL.HEADER') }}</h1> | ||||||
|       <woot-button color-scheme="primary" size="small" @click="createPortal"> |       <woot-button color-scheme="primary" size="small" @click="addPortal"> | ||||||
|         {{ $t('HELP_CENTER.PORTAL.NEW_BUTTON') }} |         {{ $t('HELP_CENTER.PORTAL.NEW_BUTTON') }} | ||||||
|       </woot-button> |       </woot-button> | ||||||
|     </div> |     </div> | ||||||
| @@ -22,6 +22,9 @@ | |||||||
|         :title="$t('HELP_CENTER.PORTAL.NO_PORTALS_MESSAGE')" |         :title="$t('HELP_CENTER.PORTAL.NO_PORTALS_MESSAGE')" | ||||||
|       /> |       /> | ||||||
|     </div> |     </div> | ||||||
|  |     <woot-modal :show.sync="isAddModalOpen" :on-close="closeModal"> | ||||||
|  |       <add-portal :show="isAddModalOpen" @cancel="closeModal" /> | ||||||
|  |     </woot-modal> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -30,11 +33,18 @@ import { mapGetters } from 'vuex'; | |||||||
| import PortalListItem from '../../components/PortalListItem'; | import PortalListItem from '../../components/PortalListItem'; | ||||||
| import Spinner from 'shared/components/Spinner.vue'; | import Spinner from 'shared/components/Spinner.vue'; | ||||||
| import EmptyState from 'dashboard/components/widgets/EmptyState'; | import EmptyState from 'dashboard/components/widgets/EmptyState'; | ||||||
|  | import AddPortal from '../../components/AddPortal'; | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     PortalListItem, |     PortalListItem, | ||||||
|     EmptyState, |     EmptyState, | ||||||
|     Spinner, |     Spinner, | ||||||
|  |     AddPortal, | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       isAddModalOpen: false, | ||||||
|  |     }; | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     ...mapGetters({ |     ...mapGetters({ | ||||||
| @@ -50,8 +60,11 @@ export default { | |||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     createPortal() { |     addPortal() { | ||||||
|       this.$emit('create-portal'); |       this.isAddModalOpen = !this.isAddModalOpen; | ||||||
|  |     }, | ||||||
|  |     closeModal() { | ||||||
|  |       this.isAddModalOpen = false; | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -26,13 +26,20 @@ export const actions = { | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   create: async ({ commit }, params) => { |   create: async ({ commit, state, dispatch }, params) => { | ||||||
|     commit(types.SET_UI_FLAG, { isCreating: true }); |     commit(types.SET_UI_FLAG, { isCreating: true }); | ||||||
|     try { |     try { | ||||||
|       const { data } = await portalAPIs.create(params); |       const { data } = await portalAPIs.create(params); | ||||||
|       const { id: portalId } = data; |       const { id: portalId } = data; | ||||||
|       commit(types.ADD_PORTAL_ENTRY, data); |       commit(types.ADD_PORTAL_ENTRY, data); | ||||||
|       commit(types.ADD_PORTAL_ID, portalId); |       commit(types.ADD_PORTAL_ID, portalId); | ||||||
|  |       const { | ||||||
|  |         portals: { selectedPortalId }, | ||||||
|  |       } = state; | ||||||
|  |       // Check if there are any selected portal | ||||||
|  |       if (!selectedPortalId) { | ||||||
|  |         dispatch('setPortalId', portalId); | ||||||
|  |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       throwErrorMessage(error); |       throwErrorMessage(error); | ||||||
|     } finally { |     } finally { | ||||||
|   | |||||||
| @@ -43,7 +43,7 @@ describe('#actions', () => { | |||||||
|     it('sends correct actions if API is success', async () => { |     it('sends correct actions if API is success', async () => { | ||||||
|       axios.post.mockResolvedValue({ data: apiResponse.payload[1] }); |       axios.post.mockResolvedValue({ data: apiResponse.payload[1] }); | ||||||
|       await actions.create( |       await actions.create( | ||||||
|         { commit }, |         { commit, dispatch, state: { portals: { selectedPortalId: null } } }, | ||||||
|         { |         { | ||||||
|           color: 'red', |           color: 'red', | ||||||
|           custom_domain: 'domain_for_help', |           custom_domain: 'domain_for_help', | ||||||
| @@ -59,7 +59,12 @@ describe('#actions', () => { | |||||||
|     }); |     }); | ||||||
|     it('sends correct actions if API is error', async () => { |     it('sends correct actions if API is error', async () => { | ||||||
|       axios.post.mockRejectedValue({ message: 'Incorrect header' }); |       axios.post.mockRejectedValue({ message: 'Incorrect header' }); | ||||||
|       await expect(actions.create({ commit }, {})).rejects.toThrow(Error); |       await expect( | ||||||
|  |         actions.create( | ||||||
|  |           { commit, dispatch, state: { portals: { selectedPortalId: null } } }, | ||||||
|  |           {} | ||||||
|  |         ) | ||||||
|  |       ).rejects.toThrow(Error); | ||||||
|       expect(commit.mock.calls).toEqual([ |       expect(commit.mock.calls).toEqual([ | ||||||
|         [types.SET_UI_FLAG, { isCreating: true }], |         [types.SET_UI_FLAG, { isCreating: true }], | ||||||
|         [types.SET_UI_FLAG, { isCreating: false }], |         [types.SET_UI_FLAG, { isCreating: false }], | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Muhsin Keloth
					Muhsin Keloth