feat: Add the ability add new portal (#5219)
This commit is contained in:
parent
657bd44418
commit
16ad263a3a
8 changed files with 278 additions and 8 deletions
|
@ -18,8 +18,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
|||
|
||||
def create
|
||||
@portal = Current.account.portals.build(portal_params)
|
||||
render json: { error: @portal.errors.messages }, status: :unprocessable_entity and return unless @portal.valid?
|
||||
|
||||
@portal.save!
|
||||
process_attached_logo
|
||||
end
|
||||
|
|
|
@ -75,3 +75,10 @@ export const convertToCategorySlug = text => {
|
|||
.replace(/[^\w ]+/g, '')
|
||||
.replace(/ +/g, '-');
|
||||
};
|
||||
|
||||
export const convertToPortalSlug = text => {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^\w ]+/g, '')
|
||||
.replace(/ +/g, '-');
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
createPendingMessage,
|
||||
convertToAttributeSlug,
|
||||
convertToCategorySlug,
|
||||
convertToPortalSlug,
|
||||
} from '../commons';
|
||||
|
||||
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": {
|
||||
|
|
|
@ -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="header-wrap">
|
||||
<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') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
|
@ -22,6 +22,9 @@
|
|||
:title="$t('HELP_CENTER.PORTAL.NO_PORTALS_MESSAGE')"
|
||||
/>
|
||||
</div>
|
||||
<woot-modal :show.sync="isAddModalOpen" :on-close="closeModal">
|
||||
<add-portal :show="isAddModalOpen" @cancel="closeModal" />
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -30,11 +33,18 @@ import { mapGetters } from 'vuex';
|
|||
import PortalListItem from '../../components/PortalListItem';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState';
|
||||
import AddPortal from '../../components/AddPortal';
|
||||
export default {
|
||||
components: {
|
||||
PortalListItem,
|
||||
EmptyState,
|
||||
Spinner,
|
||||
AddPortal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isAddModalOpen: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
@ -50,8 +60,11 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
createPortal() {
|
||||
this.$emit('create-portal');
|
||||
addPortal() {
|
||||
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 });
|
||||
try {
|
||||
const { data } = await portalAPIs.create(params);
|
||||
const { id: portalId } = data;
|
||||
commit(types.ADD_PORTAL_ENTRY, data);
|
||||
commit(types.ADD_PORTAL_ID, portalId);
|
||||
const {
|
||||
portals: { selectedPortalId },
|
||||
} = state;
|
||||
// Check if there are any selected portal
|
||||
if (!selectedPortalId) {
|
||||
dispatch('setPortalId', portalId);
|
||||
}
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
|
|
|
@ -43,7 +43,7 @@ describe('#actions', () => {
|
|||
it('sends correct actions if API is success', async () => {
|
||||
axios.post.mockResolvedValue({ data: apiResponse.payload[1] });
|
||||
await actions.create(
|
||||
{ commit },
|
||||
{ commit, dispatch, state: { portals: { selectedPortalId: null } } },
|
||||
{
|
||||
color: 'red',
|
||||
custom_domain: 'domain_for_help',
|
||||
|
@ -59,7 +59,12 @@ describe('#actions', () => {
|
|||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
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([
|
||||
[types.SET_UI_FLAG, { isCreating: true }],
|
||||
[types.SET_UI_FLAG, { isCreating: false }],
|
||||
|
|
Loading…
Reference in a new issue