mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +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