Fix url in emails, add frontendURL helper (#19)

Fixes #16
This commit is contained in:
Pranav Raj S 2019-08-25 19:59:28 +05:30 committed by GitHub
parent 28fdc062de
commit bd7bd63aae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
89 changed files with 550 additions and 398 deletions

View file

@ -53,9 +53,20 @@ jobs:
name: eslint
command: yarn run eslint
- run:
name: brakeman
command: brakeman
# - run:
# name: brakeman
# command: brakeman
# - run:
# name: Copy files
# command: |
# cp shared/config/database.yml config/database.yml
# cp shared/config/application.yml config/application.yml
# Run rails tests
- type: shell
command: |
rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
# Store yarn / webpacker cache
- save_cache:

View file

@ -25,7 +25,8 @@ module.exports = {
'allowFirstLine': false
}
}],
'vue/html-self-closing': false
'vue/html-self-closing': 'off',
"vue/no-v-html": 'off'
},
settings: {
'import/resolver': {

1
.rspec Normal file
View file

@ -0,0 +1 @@
--require spec_helper

View file

@ -56,13 +56,17 @@ gem 'foreman'
# static analysis
gem 'brakeman'
group :development do
gem 'web-console'
gem 'letter_opener'
end
group :development, :test do
gem 'byebug', platform: :mri
gem 'letter_opener'
gem 'web-console'
gem 'listen'
gem 'spring'
gem 'spring-watcher-listen'
gem 'seed_dump'
gem 'rubocop', '~> 0.74.0', require: false
gem 'rspec-rails', '~> 3.8'
end

View file

@ -172,6 +172,7 @@ GEM
crass (1.0.4)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
diff-lcs (1.3)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (0.7.0)
@ -329,6 +330,23 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rspec-core (3.8.2)
rspec-support (~> 3.8.0)
rspec-expectations (3.8.4)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-mocks (3.8.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0)
rspec-rails (3.8.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
rspec-mocks (~> 3.8.0)
rspec-support (~> 3.8.0)
rspec-support (3.8.2)
rubocop (0.74.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
@ -456,6 +474,7 @@ DEPENDENCIES
redis-rack-cache
responders
rest-client
rspec-rails (~> 3.8)
rubocop (~> 0.74.0)
sass-rails (~> 5.0)
seed_dump

View file

@ -28,6 +28,6 @@ class ConfirmationsController < Devise::ConfirmationsController
user.reset_password_token = enc
user.reset_password_sent_at = Time.now.utc
user.save(validate: false)
"/u/auth/password/edit?config=default&redirect_url=&reset_password_token="+raw
"/app/auth/password/edit?config=default&redirect_url=&reset_password_token="+raw
end
end

View file

@ -0,0 +1,6 @@
module FrontendUrlsHelper
def frontend_url(path, **query_params)
url_params = query_params.blank? ? "" : "?#{query_params.to_query}"
"#{root_url}app/#{path}#{url_params}"
end
end

View file

@ -7,6 +7,7 @@
import moment from 'moment';
import Cookies from 'js-cookie';
import endPoints from './endPoints';
import { frontendURL } from '../helper/URLHelper';
export default {
login(creds) {
@ -65,7 +66,7 @@ export default {
if (error.response.status === 401) {
Cookies.remove('auth_data');
Cookies.remove('user');
window.location = '/login';
window.location = frontendURL('login');
}
reject(error);
});
@ -80,7 +81,7 @@ export default {
.then(response => {
Cookies.remove('auth_data');
Cookies.remove('user');
window.location = '/u/login';
window.location = frontendURL('login');
resolve(response);
})
.catch(error => {

View file

@ -1,8 +1,8 @@
<template>
<button type="submit" :disabled="disabled" :class="computedClass">
<i :class="iconClass" class="icon" v-if="!!iconClass"></i>
<span>{{buttonText}}</span>
<spinner v-if="loading"/>
<i v-if="!!iconClass" :class="iconClass" class="icon"></i>
<span>{{ buttonText }}</span>
<spinner v-if="loading" />
</button>
</template>
@ -10,19 +10,34 @@
import Spinner from '../Spinner';
export default {
props: {
disabled: Boolean,
loading: Boolean,
buttonText: String,
buttonClass: String,
iconClass: String,
},
components: {
Spinner,
},
props: {
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
buttonText: {
type: String,
default: '',
},
buttonClass: {
type: String,
default: '',
},
iconClass: {
type: String,
default: '',
},
},
computed: {
computedClass() {
return `button ${this.buttonClass || ' '}`;
return `button nice ${this.buttonClass || ' '}`;
},
},
};

View file

@ -1,7 +1,12 @@
<template>
<button type="button" v-on:click="toggleStatus" class="button round resolve--button" :class="buttonClass">
<i class="icon" :class="buttonIconClass" v-if="!isLoading"></i>
<spinner v-if="isLoading"/>
<button
type="button"
class="button nice resolve--button"
:class="buttonClass"
@click="toggleStatus"
>
<i v-if="!isLoading" class="icon" :class="buttonIconClass"></i>
<spinner v-if="isLoading" />
{{ currentStatus }}
</button>
</template>
@ -13,9 +18,7 @@ import { mapGetters } from 'vuex';
import Spinner from '../Spinner';
export default {
props: [
'conversationId',
],
props: ['conversationId'],
data() {
return {
isLoading: false,
@ -50,5 +53,3 @@ export default {
},
};
</script>

View file

@ -1,47 +1,56 @@
<template>
<aside class="sidebar animated shrink columns">
<div class="logo">
<router-link to="{ path: '/u/dashboard' }" replace>
<img src="~assets/images/woot-logo.svg" alt="Woot-logo"/>
<router-link :to="dashboardPath" replace>
<img src="~assets/images/woot-logo.svg" alt="Woot-logo" />
</router-link>
</div>
<div class="main-nav">
<transition-group name="menu-list" tag="ul" class="menu vertical">
<sidebar-item
v-for="item in sidebarItems"
v-for="item in accessibleMenuItems"
:key="item.toState"
:menu-item="item"
:key="item"
v-if="showItem(item)"
/>
</transition-group>
</div>
<transition name="fade" mode="out-in">
<woot-status-bar
v-if="shouldShowStatusBox"
:message="trialMessage"
:buttonText="$t('APP_GLOBAL.TRAIL_BUTTON')"
:buttonRoute="{ name: 'billing' }"
:button-text="$t('APP_GLOBAL.TRAIL_BUTTON')"
:button-route="{ name: 'billing' }"
:type="statusBarClass"
:show-button="isAdmin()"
v-if="shouldShowStatusBox"
/>
</transition>
<div class="bottom-nav">
<transition name="menu-slide">
<div class="dropdown-pane top" v-if="showOptionsMenu" v-on-clickaway="showOptions">
<div
v-if="showOptionsMenu"
v-on-clickaway="showOptions"
class="dropdown-pane top"
>
<ul class="vertical dropdown menu">
<li><a href="#">Help & Support</a></li>
<!-- <li><a href="#">Help & Support</a></li> -->
<li><a href="#" @click.prevent="logout()">Logout</a></li>
</ul>
</div>
</transition>
<div class="current-user" @click.prevent="showOptions()">
<img class="current-user--thumbnail" :src="gravatarUrl()"/>
<img class="current-user--thumbnail" :src="gravatarUrl()" />
<div class="current-user--data">
<h3 class="current-user--name">{{ currentUser.name }}</h3>
<h5 class="current-user--role">{{ currentUser.role }}</h5>
<h3 class="current-user--name">
{{ currentUser.name }}
</h3>
<h5 class="current-user--role">
{{ currentUser.role }}
</h5>
</div>
<span class="current-user--options icon ion-android-more-vertical"></span>
<span
class="current-user--options icon ion-android-more-vertical"
></span>
</div>
<!-- <router-link class="icon ion-arrow-graph-up-right" tag="span" to="/settings/reports" active-class="active"></router-link> -->
</div>
@ -57,17 +66,20 @@ import adminMixin from '../../mixins/isAdmin';
import Auth from '../../api/auth';
import SidebarItem from './SidebarItem';
import WootStatusBar from '../widgets/StatusBar';
/* eslint-disable no-console */
import { frontendURL } from '../../helper/URLHelper';
export default {
mixins: [clickaway, adminMixin],
props: {
route: String,
route: {
type: String,
},
},
data() {
return {
showOptionsMenu: false,
};
},
mixins: [clickaway, adminMixin],
mounted() {
// this.$store.dispatch('fetchLabels');
this.$store.dispatch('fetchInboxes');
@ -78,22 +90,30 @@ export default {
daysLeft: 'getTrialLeft',
subscriptionData: 'getSubscription',
}),
sidebarItems() {
// Get Current Route
accessibleMenuItems() {
const currentRoute = this.$store.state.route.name;
// get all keys in menuGroup
const groupKey = Object.keys(this.sidebarList);
let menuItems = [];
// Iterate over menuGroup to find the correct group
for (let i = 0; i < groupKey.length; i += 1) {
const groupItem = this.sidebarList[groupKey[i]];
// Check if current route is included
const isRouteIncluded = groupItem.routes.indexOf(currentRoute) > -1;
if (isRouteIncluded) {
return groupItem.menuItems;
menuItems = Object.values(groupItem.menuItems);
}
}
// If not found return empty array
return [];
const { role } = this.currentUser;
return menuItems.filter(
menuItem =>
window.roleWiseRoutes[role].indexOf(menuItem.toStateName) > -1
);
},
dashboardPath() {
return frontendURL('dashboard');
},
currentUser() {
return Auth.getCurrentUser();
@ -102,12 +122,16 @@ export default {
return `${this.daysLeft} ${this.$t('APP_GLOBAL.TRIAL_MESSAGE')}`;
},
shouldShowStatusBox() {
return this.subscriptionData.state === 'trial' || this.subscriptionData.state === 'cancelled';
return (
this.subscriptionData.state === 'trial' ||
this.subscriptionData.state === 'cancelled'
);
},
statusBarClass() {
if (this.subscriptionData.state === 'trial') {
return 'warning';
} else if (this.subscriptionData.state === 'cancelled') {
}
if (this.subscriptionData.state === 'cancelled') {
return 'danger';
}
return '';
@ -122,11 +146,6 @@ export default {
const hash = md5(this.currentUser.email);
return `${window.WootConstants.GRAVATAR_URL}${hash}?d=monsterid`;
},
// Show if user has access to the route
showItem(item) {
const { role } = this.currentUser;
return window.roleWiseRoutes[role].indexOf(item.toStateName) > -1;
},
showOptions() {
this.showOptionsMenu = !this.showOptionsMenu;
},

View file

@ -46,7 +46,7 @@
<span v-if="isAdmin()">
{{ $t('CONVERSATION.NO_INBOX_1') }}
<br />
<router-link to="/u/settings/inboxes/new">
<router-link :to="newInboxURL">
{{ $t('CONVERSATION.CLICK_HERE') }}
</router-link>
{{ $t('CONVERSATION.NO_INBOX_2') }}
@ -88,6 +88,7 @@ import ReplyBox from './ReplyBox';
import Conversation from './Conversation';
import conversationMixin from '../../../mixins/conversations';
import adminMixin from '../../../mixins/isAdmin';
import { frontendURL } from '../../../helper/URLHelper';
export default {
components: {
@ -169,6 +170,10 @@ export default {
);
},
newInboxURL() {
return frontendURL('settings/inboxes/new');
},
shouldLoadMoreChats() {
return !this.listLoadingStatus && !this.isLoadingPrevious;
},

View file

@ -1,11 +1,34 @@
<template>
<div class="conversation" :class="{ active: isActiveChat, 'unread-chat': hasUnread }" @click="cardClick(chat)" >
<Thumbnail :src="chat.meta.sender.thumbnail" :badge="chat.meta.sender.channel" class="columns" />
<div
class="conversation"
:class="{ active: isActiveChat, 'unread-chat': hasUnread }"
@click="cardClick(chat)"
>
<Thumbnail
:src="chat.meta.sender.thumbnail"
:badge="chat.meta.sender.channel"
class="columns"
/>
<div class="conversation--details columns">
<h4 class="conversation--user">{{ chat.meta.sender.name }} <span class="label" v-tooltip.bottom="inboxName(chat.inbox_id)" v-if="isInboxNameVisible">{{inboxName(chat.inbox_id)}}</span> </h4>
<p class="conversation--message" v-html="extractMessageText(lastMessage(chat))"></p>
<h4 class="conversation--user">
{{ chat.meta.sender.name }}
<span
v-if="isInboxNameVisible"
v-tooltip.bottom="inboxName(chat.inbox_id)"
class="label"
>
{{ inboxName(chat.inbox_id) }}
</span>
</h4>
<p
class="conversation--message"
v-html="extractMessageText(lastMessage(chat))"
></p>
<div class="conversation--meta">
<span class="timestamp">{{ dynamicTime(lastMessage(chat).created_at) }}</span>
<span class="timestamp">
{{ dynamicTime(lastMessage(chat).created_at) }}
</span>
<span class="unread">{{ getUnreadCount }}</span>
</div>
</div>
@ -14,27 +37,25 @@
<script>
/* eslint no-console: 0 */
/* eslint no-extra-boolean-cast: 0 */
/* global bus */
import { mapGetters } from 'vuex';
import Thumbnail from '../Thumbnail';
import getEmojiSVG from '../emoji/utils';
import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time';
import router from '../../../routes';
import { frontendURL } from '../../../helper/URLHelper';
export default {
props: [
'chat',
],
mixins: [timeMixin, conversationMixin],
components: {
Thumbnail,
},
created() {
// console.log(this.chat.inbox_id);
mixins: [timeMixin, conversationMixin],
props: {
chat: {
type: Object,
default: () => {},
},
},
computed: {
@ -64,7 +85,7 @@ export default {
methods: {
cardClick(chat) {
router.push({
path: `/u/conversations/${chat.id}`,
path: frontendURL(`conversations/${chat.id}`),
});
},
extractMessageText(chatItem) {
@ -85,7 +106,9 @@ export default {
},
getEmojiSVG,
inboxName(inboxId) {
const [stateInbox] = this.inboxesList.filter(inbox => inbox.channel_id === inboxId);
const [stateInbox] = this.inboxesList.filter(
inbox => inbox.channel_id === inboxId
);
return !stateInbox ? '' : stateInbox.label;
},
},

View file

@ -0,0 +1,6 @@
import queryString from 'query-string';
export const frontendURL = (path, params) => {
const stringifiedParams = params ? `?${queryString.stringify(params)}` : '';
return `/app/${path}${stringifiedParams}`;
};

View file

@ -1,3 +1,5 @@
import { frontendURL } from '../helper/URLHelper';
export default {
menuGroup: {
common: {
@ -14,7 +16,7 @@ export default {
label: 'Conversations',
hasSubMenu: false,
key: '',
toState: '/u/dashboard',
toState: frontendURL('dashboard'),
toolTip: 'Conversation from all subscribed inboxes',
toStateName: 'home',
},
@ -22,14 +24,14 @@ export default {
icon: 'ion-arrow-graph-up-right',
label: 'Reports',
hasSubMenu: false,
toState: '/u/reports',
toState: frontendURL('reports'),
toStateName: 'settings_account_reports',
},
settings: {
icon: 'ion-settings',
label: 'Settings',
hasSubMenu: false,
toState: '/u/settings/',
toState: frontendURL('settings'),
toStateName: 'settings_home',
},
inbox: {
@ -39,7 +41,7 @@ export default {
newLink: true,
key: 'inbox',
cssClass: 'menu-title align-justify',
toState: '/u/settings/inboxes',
toState: frontendURL('settings/inboxes'),
toStateName: 'settings_inbox_list',
children: [],
},
@ -65,41 +67,41 @@ export default {
label: 'Home',
hasSubMenu: false,
toStateName: 'home',
toState: '/u/dashboard',
toState: frontendURL('dashboard'),
},
agents: {
icon: 'ion-person-stalker',
label: 'Agents',
hasSubMenu: false,
toState: '/u/settings/agents/list',
toState: frontendURL('settings/agents/list'),
toStateName: 'agent_list',
},
inboxes: {
icon: 'ion-archive',
label: 'Inboxes',
hasSubMenu: false,
toState: '/u/settings/inboxes/list',
toState: frontendURL('settings/inboxes/list'),
toStateName: 'settings_inbox_list',
},
cannedResponses: {
icon: 'ion-chatbox-working',
label: 'Canned Responses',
hasSubMenu: false,
toState: '/u/settings/canned-response/list',
toState: frontendURL('settings/canned-response/list'),
toStateName: 'canned_list',
},
billing: {
icon: 'ion-card',
label: 'Billing',
hasSubMenu: false,
toState: '/u/settings/billing',
toState: frontendURL('settings/billing'),
toStateName: 'billing',
},
account: {
icon: 'ion-beer',
label: 'Account Settings',
hasSubMenu: false,
toState: '/u/settings/account',
toState: frontendURL('settings/account'),
toStateName: 'account',
},
},

View file

@ -1,7 +1,7 @@
{
"INBOX_MGMT": {
"HEADER": "Inboxes",
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a Facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a new Facebook Page. </p><p> In the <a href=\"/u/dashboard\">Dashboard</a>, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a Facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a new Facebook Page. </p><p> In the <a href=\"/app/dashboard\">Dashboard</a>, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
"LIST": {
"404": "There are no inboxes attached to this account."
},

View file

@ -1,13 +1,20 @@
<template>
<form class="login-box medium-4 column align-self-middle" v-on:submit.prevent="submit()">
<h4>{{$t('RESET_PASSWORD.TITLE')}}</h4>
<form
class="login-box medium-4 column align-self-middle"
@submit.prevent="submit()"
>
<h4>{{ $t('RESET_PASSWORD.TITLE') }}</h4>
<div class="column log-in-form">
<label :class="{ 'error': $v.credentials.email.$error }">
{{$t('RESET_PASSWORD.EMAIL.LABEL')}}
<input type="text" v-bind:placeholder="$t('RESET_PASSWORD.EMAIL.PLACEHOLDER')" v-model.trim="credentials.email" @input="$v.credentials.email.$touch">
<span class="message" v-if="$v.credentials.email.$error">
{{$t('RESET_PASSWORD.EMAIL.ERROR')}}
<label :class="{ error: $v.credentials.email.$error }">
{{ $t('RESET_PASSWORD.EMAIL.LABEL') }}
<input
v-model.trim="credentials.email"
type="text"
:placeholder="$t('RESET_PASSWORD.EMAIL.PLACEHOLDER')"
@input="$v.credentials.email.$touch"
/>
<span v-if="$v.credentials.email.$error" class="message">
{{ $t('RESET_PASSWORD.EMAIL.ERROR') }}
</span>
</label>
<woot-submit-button
@ -24,6 +31,7 @@
/* global bus */
import { required, minLength, email } from 'vuelidate/lib/validators';
import Auth from '../../api/auth';
import { frontendURL } from '../../helper/URLHelper';
export default {
data() {
@ -58,13 +66,13 @@ export default {
submit() {
this.resetPassword.showLoading = true;
Auth.resetPassword(this.credentials)
.then((res) => {
let message = this.$t('RESET_PASSWORD.API.SUCCESS_MESSAGE');
.then(res => {
let successMessage = this.$t('RESET_PASSWORD.API.SUCCESS_MESSAGE');
if (res.data && res.data.message) {
message = res.data.message;
successMessage = res.data.message;
}
this.showAlert(message);
window.location = '/login';
this.showAlert(successMessage);
window.location = frontendURL('login');
})
.catch(() => {
this.showAlert(this.$t('RESET_PASSWORD.API.ERROR_MESSAGE'));

View file

@ -1,9 +1,17 @@
<template>
<div class="medium-10 column signup">
<div class="text-center medium-12 signup__hero">
<img src="~assets/images/woot-logo.svg" alt="Woot-logo" class="hero__logo" />
<h2 class="hero__title">{{$t('REGISTER.TRY_WOOT')}}</h2>
<p class="hero__sub">{{$t('REGISTER.TRY_WOOT_SUB')}}</p>
<img
src="~assets/images/woot-logo.svg"
alt="Woot-logo"
class="hero__logo"
/>
<h2 class="hero__title">
{{ $t('REGISTER.TRY_WOOT') }}
</h2>
<p class="hero__sub">
{{ $t('REGISTER.TRY_WOOT_SUB') }}
</p>
</div>
<div class="row align-center">
<div class="medium-5 column">
@ -16,24 +24,38 @@
</ul>
</div>
<div class="medium-5 column">
<form class="signup-box login-box " v-on:submit.prevent="submit()">
<form class="signup-box login-box " @submit.prevent="submit()">
<div class="column log-in-form">
<label :class="{ 'error': $v.credentials.name.$error }">
{{$t('REGISTER.ACCOUNT_NAME.LABEL')}}
<input type="text" v-bind:placeholder="$t('REGISTER.ACCOUNT_NAME.PLACEHOLDER')" v-model.trim="credentials.name" @input="$v.credentials.name.$touch">
<span class="message" v-if="$v.credentials.name.$error">
{{$t('REGISTER.ACCOUNT_NAME.ERROR')}}
<label :class="{ error: $v.credentials.name.$error }">
{{ $t('REGISTER.ACCOUNT_NAME.LABEL') }}
<input
v-model.trim="credentials.name"
type="text"
:placeholder="$t('REGISTER.ACCOUNT_NAME.PLACEHOLDER')"
@input="$v.credentials.name.$touch"
/>
<span v-if="$v.credentials.name.$error" class="message">
{{ $t('REGISTER.ACCOUNT_NAME.ERROR') }}
</span>
</label>
<label :class="{ 'error': $v.credentials.email.$error }">
{{$t('REGISTER.EMAIL.LABEL')}}
<input type="email" v-bind:placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')" v-model.trim="credentials.email" @input="$v.credentials.email.$touch">
<span class="message" v-if="$v.credentials.email.$error">
{{$t('REGISTER.EMAIL.ERROR')}}
<label :class="{ error: $v.credentials.email.$error }">
{{ $t('REGISTER.EMAIL.LABEL') }}
<input
v-model.trim="credentials.email"
type="email"
:placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')"
@input="$v.credentials.email.$touch"
/>
<span v-if="$v.credentials.email.$error" class="message">
{{ $t('REGISTER.EMAIL.ERROR') }}
</span>
</label>
<woot-submit-button
:disabled="$v.credentials.name.$invalid || $v.credentials.email.$invalid || register.showLoading"
:disabled="
$v.credentials.name.$invalid ||
$v.credentials.email.$invalid ||
register.showLoading
"
:button-text="$t('REGISTER.SUBMIT')"
:loading="register.showLoading"
button-class="large expanded"
@ -58,6 +80,7 @@
import { required, minLength, email } from 'vuelidate/lib/validators';
import Auth from '../../api/auth';
import { frontendURL } from '../../helper/URLHelper';
export default {
data() {
@ -96,12 +119,12 @@ export default {
submit() {
this.register.showLoading = true;
Auth.register(this.credentials)
.then((res) => {
.then(res => {
if (res.status === 200) {
window.location = '/u/dashboard';
window.location = frontendURL('dashboard');
}
})
.catch((error) => {
.catch(error => {
let errorMessage = this.$t('REGISTER.API.ERROR_MESSAGE');
if (error.response && error.response.data.message) {
errorMessage = error.response.data.message;

View file

@ -3,11 +3,12 @@ import Confirmation from './Confirmation';
import Signup from './Signup';
import PasswordEdit from './PasswordEdit';
import ResetPassword from './ResetPassword';
import { frontendURL } from '../../helper/URLHelper';
export default {
routes: [
{
path: '/u/auth',
path: frontendURL('auth'),
name: 'auth',
component: Auth,
children: [

View file

@ -1,10 +1,11 @@
/* eslint arrow-body-style: 0 */
import ConversationView from './ConversationView';
import { frontendURL } from '../../../helper/URLHelper';
export default {
routes: [
{
path: '/u/dashboard',
path: frontendURL('dashboard'),
name: 'home',
roles: ['administrator', 'agent'],
component: ConversationView,
@ -13,7 +14,7 @@ export default {
},
},
{
path: '/u/inbox/:inbox_id',
path: frontendURL('inbox/:inbox_id'),
name: 'inbox_dashboard',
roles: ['administrator', 'agent'],
component: ConversationView,
@ -22,7 +23,7 @@ export default {
},
},
{
path: '/u/conversations/:conversation_id',
path: frontendURL('conversations/:conversation_id'),
name: 'inbox_conversation',
roles: ['administrator', 'agent'],
component: ConversationView,

View file

@ -1,11 +1,12 @@
import AppContainer from './Dashboard';
import settings from './settings/settings.routes';
import conversation from './conversation/conversation.routes';
import { frontendURL } from '../../helper/URLHelper';
export default {
routes: [
{
path: '/u/',
path: frontendURL(''),
component: AppContainer,
children: [...conversation.routes, ...settings.routes],
},

View file

@ -1,7 +1,7 @@
<template>
<div class="column content-box">
<button
class="button icon success btn-fixed-right-top"
class="button nice icon success btn-fixed-right-top"
@click="openAddPopup()"
>
<i class="icon ion-android-add-circle"></i>
@ -12,13 +12,20 @@
<!-- List Agents -->
<div class="row">
<div class="small-8 columns">
<woot-loading-state v-if="fetchStatus" :message="$t('AGENT_MGMT.LOADING')" />
<p v-if="!fetchStatus && !agentList.length">{{ $t('AGENT_MGMT.LIST.404') }}</p>
<table class="woot-table" v-if="!fetchStatus && agentList.length">
<woot-loading-state
v-if="fetchStatus"
:message="$t('AGENT_MGMT.LOADING')"
/>
<p v-if="!fetchStatus && !agentList.length">
{{ $t('AGENT_MGMT.LIST.404') }}
</p>
<table v-if="!fetchStatus && agentList.length" class="woot-table">
<tbody>
<tr v-for="(agent, index) in agentList">
<tr v-for="(agent, index) in agentList" :key="agent.email">
<!-- Gravtar Image -->
<td><img class="woot-thumbnail" :src="gravatarUrl(agent.email)"/></td>
<td>
<img class="woot-thumbnail" :src="gravatarUrl(agent.email)" />
</td>
<!-- Agent Name + Email -->
<td>
<span class="agent-name">{{ agent.name }}</span>
@ -27,8 +34,12 @@
<!-- Agent Role + Verification Status -->
<td>
<span class="agent-name">{{ agent.role }}</span>
<span v-if="agent.confirmed">{{$t('AGENT_MGMT.LIST.VERIFIED')}}</span>
<span v-if="!agent.confirmed">{{$t('AGENT_MGMT.LIST.VERIFICATION_PENDING')}}</span>
<span v-if="agent.confirmed">
{{ $t('AGENT_MGMT.LIST.VERIFIED') }}
</span>
<span v-if="!agent.confirmed">
{{ $t('AGENT_MGMT.LIST.VERIFICATION_PENDING') }}
</span>
</td>
<!-- Actions -->
<td>
@ -60,18 +71,18 @@
</div>
<!-- Add Agent -->
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
<add-agent :on-close="hideAddPopup"/>
<add-agent :on-close="hideAddPopup" />
</woot-modal>
<!-- Edit Agent -->
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
<edit-agent
v-if="showEditPopup"
:id="currentAgent.id"
:name="currentAgent.name"
:type="currentAgent.role"
:email="currentAgent.email"
:id="currentAgent.id"
:on-close="hideEditPopup"
v-if="showEditPopup"
/>
</woot-modal>
@ -87,9 +98,7 @@
/>
<!-- Loader Status -->
</div>
</template>
<script>
/* global bus */
@ -97,14 +106,12 @@
import { mapGetters } from 'vuex';
import md5 from 'md5';
import PageHeader from '../SettingsSubPageHeader';
import AddAgent from './AddAgent';
import EditAgent from './EditAgent';
import DeleteAgent from './DeleteAgent';
export default {
components: {
PageHeader,
AddAgent,
EditAgent,
DeleteAgent,
@ -127,13 +134,19 @@ export default {
fetchStatus: 'getAgentFetchStatus',
}),
deleteConfirmText() {
return `${this.$t('AGENT_MGMT.DELETE.CONFIRM.YES')} ${this.currentAgent.name}`;
return `${this.$t('AGENT_MGMT.DELETE.CONFIRM.YES')} ${
this.currentAgent.name
}`;
},
deleteRejectText() {
return `${this.$t('AGENT_MGMT.DELETE.CONFIRM.NO')} ${this.currentAgent.name}`;
return `${this.$t('AGENT_MGMT.DELETE.CONFIRM.NO')} ${
this.currentAgent.name
}`;
},
deleteMessage() {
return `${this.$t('AGENT_MGMT.DELETE.CONFIRM.MESSAGE')} ${this.currentAgent.name} ?`;
return `${this.$t('AGENT_MGMT.DELETE.CONFIRM.MESSAGE')} ${
this.currentAgent.name
} ?`;
},
},
mounted() {
@ -142,7 +155,9 @@ export default {
methods: {
showActions(agent) {
if (agent.role === 'administrator') {
const adminList = this.agentList.filter(item => item.role === 'administrator');
const adminList = this.agentList.filter(
item => item.role === 'administrator'
);
return adminList.length !== 1;
}
return true;
@ -184,11 +199,14 @@ export default {
this.deleteAgent(this.currentAgent.id);
},
deleteAgent(id) {
this.$store.dispatch('deleteAgent', {
this.$store
.dispatch('deleteAgent', {
id,
}).then(() => {
})
.then(() => {
this.showAlert(this.$t('AGENT_MGMT.DELETE.API.SUCCESS_MESSAGE'));
}).catch(() => {
})
.catch(() => {
this.showAlert(this.$t('AGENT_MGMT.DELETE.API.ERROR_MESSAGE'));
});
},
@ -196,7 +214,7 @@ export default {
showAlert(message) {
// Reset loading, current selected agent
this.loading[this.currentAgent.id] = false;
this.currentAgent = { };
this.currentAgent = {};
// Show message
this.agentAPI.message = message;
bus.$emit('newToastMessage', message);

View file

@ -1,10 +1,11 @@
import SettingsContent from '../Wrapper';
import AgentHome from './Index';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
routes: [
{
path: '/u/settings/agents',
path: frontendURL('settings/agents'),
component: SettingsContent,
props: {
headerTitle: 'AGENT_MGMT.HEADER',

View file

@ -1,11 +1,12 @@
import Index from './Index';
import SettingsContent from '../Wrapper';
import AccountLocked from './AccountLocked';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
routes: [
{
path: '/u/settings/billing',
path: frontendURL('settings/billing'),
component: SettingsContent,
props: {
headerTitle: 'BILLING.HEADER',

View file

@ -1,7 +1,7 @@
<template>
<div class="column content-box">
<button
class="button icon success btn-fixed-right-top"
class="button nice icon success btn-fixed-right-top"
@click="openAddPopup()"
>
<i class="icon ion-android-add-circle"></i>
@ -10,27 +10,42 @@
<!-- List Canned Response -->
<div class="row">
<div class="small-8 columns">
<p v-if="!fetchStatus && !cannedResponseList.length" class="no-items-error-message">
<p
v-if="!fetchStatus && !cannedResponseList.length"
class="no-items-error-message"
>
{{ $t('CANNED_MGMT.LIST.404') }}
</p>
<woot-loading-state v-if="fetchStatus" :message="$t('CANNED_MGMT.LOADING')" />
<woot-loading-state
v-if="fetchStatus"
:message="$t('CANNED_MGMT.LOADING')"
/>
<table class="woot-table" v-if="!fetchStatus && cannedResponseList.length">
<table
v-if="!fetchStatus && cannedResponseList.length"
class="woot-table"
>
<thead>
<!-- Header -->
<th v-for="thHeader in $t('CANNED_MGMT.LIST.TABLE_HEADER')">
<th
v-for="thHeader in $t('CANNED_MGMT.LIST.TABLE_HEADER')"
:key="thHeader"
>
{{ thHeader }}
</th>
</thead>
<tbody>
<tr v-for="(cannedItem, index) in cannedResponseList">
<tr
v-for="(cannedItem, index) in cannedResponseList"
:key="cannedItem.short_code"
>
<!-- Short Code -->
<td>{{ cannedItem.short_code }}</td>
<!-- Content -->
<td>{{ cannedItem.content }}</td>
<!-- Action Buttons -->
<td class="button-wrapper">
<div @click="openEditPopup(cannedItem)" >
<div @click="openEditPopup(cannedItem)">
<woot-submit-button
:button-text="$t('CANNED_MGMT.EDIT.BUTTON_TEXT')"
icon-class="ion-edit"
@ -57,17 +72,17 @@
</div>
<!-- Add Agent -->
<woot-modal :show.sync="showAddPopup" :on-close="hideAddPopup">
<add-canned :on-close="hideAddPopup"/>
<add-canned :on-close="hideAddPopup" />
</woot-modal>
<!-- Edit Canned Response -->
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
<edit-canned
v-if="showEditPopup"
:id="selectedResponse.id"
:edshort-code="selectedResponse.short_code"
:edcontent="selectedResponse.content"
:id="selectedResponse.id"
:on-close="hideEditPopup"
v-if="showEditPopup"
/>
</woot-modal>
@ -82,13 +97,11 @@
:reject-text="deleteRejectText"
/>
</div>
</template>
<script>
/* global bus */
import { mapGetters } from 'vuex';
import PageHeader from '../SettingsSubPageHeader';
import AddCanned from './AddCanned';
import EditCanned from './EditCanned';
import DeleteCanned from './DeleteCanned';
@ -96,7 +109,6 @@ import DeleteCanned from './DeleteCanned';
export default {
components: {
AddCanned,
PageHeader,
EditCanned,
DeleteCanned,
},
@ -119,13 +131,19 @@ export default {
}),
// Delete Modal
deleteConfirmText() {
return `${this.$t('CANNED_MGMT.DELETE.CONFIRM.YES')} ${this.selectedResponse.short_code}`;
return `${this.$t('CANNED_MGMT.DELETE.CONFIRM.YES')} ${
this.selectedResponse.short_code
}`;
},
deleteRejectText() {
return `${this.$t('CANNED_MGMT.DELETE.CONFIRM.NO')} ${this.selectedResponse.short_code}`;
return `${this.$t('CANNED_MGMT.DELETE.CONFIRM.NO')} ${
this.selectedResponse.short_code
}`;
},
deleteMessage() {
return `${this.$t('CANNED_MGMT.DELETE.CONFIRM.MESSAGE')} ${this.selectedResponse.short_code} ?`;
return `${this.$t('CANNED_MGMT.DELETE.CONFIRM.MESSAGE')} ${
this.selectedResponse.short_code
} ?`;
},
},
mounted() {
@ -136,7 +154,7 @@ export default {
showAlert(message) {
// Reset loading, current selected agent
this.loading[this.selectedResponse.id] = false;
this.selectedResponse = { };
this.selectedResponse = {};
// Show message
this.cannedResponseAPI.message = message;
bus.$emit('newToastMessage', message);
@ -173,11 +191,14 @@ export default {
this.deleteCannedResponse(this.selectedResponse.id);
},
deleteCannedResponse(id) {
this.$store.dispatch('deleteCannedResponse', {
this.$store
.dispatch('deleteCannedResponse', {
id,
}).then(() => {
})
.then(() => {
this.showAlert(this.$t('CANNED_MGMT.DELETE.API.SUCCESS_MESSAGE'));
}).catch(() => {
})
.catch(() => {
this.showAlert(this.$t('CANNED_MGMT.DELETE.API.ERROR_MESSAGE'));
});
},

View file

@ -1,10 +1,11 @@
import SettingsContent from '../Wrapper';
import CannedHome from './Index';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
routes: [
{
path: '/u/settings/canned-response',
path: frontendURL('settings/canned-response'),
component: SettingsContent,
props: {
headerTitle: 'CANNED_MGMT.HEADER',

View file

@ -5,15 +5,24 @@
<div class="small-8 columns">
<p v-if="!inboxesList.length" class="no-items-error-message">
{{ $t('INBOX_MGMT.LIST.404') }}
<router-link to="/u/settings/inboxes/new" v-if="isAdmin()">
<router-link
v-if="isAdmin()"
:to="frontendURL('settings/inboxes/new')"
>
{{ $t('SETTINGS.INBOXES.NEW_INBOX') }}
</router-link>
</p>
<table class="woot-table" v-if="inboxesList.length">
<table v-if="inboxesList.length" class="woot-table">
<tbody>
<tr v-for="item in inboxesList">
<td><img class="woot-thumbnail" :src="item.avatarUrl" alt="No Page Image"/></td>
<tr v-for="item in inboxesList" :key="item.label">
<td>
<img
class="woot-thumbnail"
:src="item.avatarUrl"
alt="No Page Image"
/>
</td>
<!-- Short Code -->
<td>
<span class="agent-name">{{ item.label }}</span>
@ -23,7 +32,7 @@
<!-- Action Buttons -->
<td>
<div class="button-wrapper">
<div @click="openSettings(item)" v-if="isAdmin()">
<div v-if="isAdmin()" @click="openSettings(item)">
<woot-submit-button
:button-text="$t('INBOX_MGMT.SETTINGS')"
icon-class="ion-gear-b"
@ -37,7 +46,7 @@
button-class="link hollow grey-btn"
/>
</div> -->
<div @click="openDelete(item)" v-if="isAdmin()">
<div v-if="isAdmin()" @click="openDelete(item)">
<woot-submit-button
:button-text="$t('INBOX_MGMT.DELETE.BUTTON_TEXT')"
:loading="loading[item.id]"
@ -57,10 +66,10 @@
</div>
</div>
<settings
v-if="showSettings"
:show.sync="showSettings"
:on-close="closeSettings"
:inbox="selectedInbox"
v-if="showSettings"
/>
<delete-inbox
@ -78,14 +87,13 @@
/* global bus */
import { mapGetters } from 'vuex';
import InboxListItem from '../../../../components/widgets/InboxListItem';
import Settings from './Settings';
import DeleteInbox from './DeleteInbox';
import adminMixin from '../../../../mixins/isAdmin';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
components: {
InboxListItem,
Settings,
DeleteInbox,
},
@ -104,13 +112,19 @@ export default {
}),
// Delete Modal
deleteConfirmText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.YES')} ${this.selectedInbox.label}`;
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.YES')} ${
this.selectedInbox.label
}`;
},
deleteRejectText() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.NO')} ${this.selectedInbox.label}`;
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.NO')} ${
this.selectedInbox.label
}`;
},
deleteMessage() {
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.MESSAGE')} ${this.selectedInbox.label} ?`;
return `${this.$t('INBOX_MGMT.DELETE.CONFIRM.MESSAGE')} ${
this.selectedInbox.label
} ?`;
},
},
methods: {
@ -123,9 +137,20 @@ export default {
this.selectedInbox = {};
},
deleteInbox({ channel_id }) {
this.$store.dispatch('deleteInbox', channel_id)
.then(() => bus.$emit('newToastMessage', this.$t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE')))
.catch(() => bus.$emit('newToastMessage', this.$t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE')));
this.$store
.dispatch('deleteInbox', channel_id)
.then(() =>
bus.$emit(
'newToastMessage',
this.$t('INBOX_MGMT.DELETE.API.SUCCESS_MESSAGE')
)
)
.catch(() =>
bus.$emit(
'newToastMessage',
this.$t('INBOX_MGMT.DELETE.API.ERROR_MESSAGE')
)
);
},
confirmDeletion() {
@ -140,6 +165,7 @@ export default {
this.showDeletePopup = false;
this.selectedInbox = {};
},
frontendURL,
},
};
</script>

View file

@ -6,11 +6,12 @@ import ChannelList from './ChannelList';
import channelFactory from './channel-factory';
import AddAgents from './AddAgents';
import FinishSetup from './FinishSetup';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
routes: [
{
path: '/u/settings/inboxes',
path: frontendURL('settings/inboxes'),
component: SettingsContent,
props: {
headerTitle: 'INBOX_MGMT.HEADER',

View file

@ -1,10 +1,11 @@
import Index from './Index';
import SettingsContent from '../Wrapper';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
routes: [
{
path: '/u/reports',
path: frontendURL('reports'),
component: SettingsContent,
props: {
headerTitle: 'REPORT.HEADER',

View file

@ -4,18 +4,19 @@ import canned from './canned/canned.routes';
import reports from './reports/reports.routes';
import billing from './billing/billing.routes';
import Auth from '../../../api/auth';
import { frontendURL } from '../../../helper/URLHelper';
export default {
routes: [
{
path: '/u/settings',
path: frontendURL('settings'),
name: 'settings_home',
roles: ['administrator', 'agent'],
redirect: () => {
if (Auth.isAdmin()) {
return '/u/settings/agents/';
return frontendURL('settings/agents');
}
return '/u/settings/canned-response';
return frontendURL('settings/canned-response');
},
},
...inbox.routes,

View file

@ -5,6 +5,7 @@ import auth from '../api/auth';
import login from './login/login.routes';
import dashboard from './dashboard/dashboard.routes';
import authRoute from './auth/auth.routes';
import { frontendURL } from '../helper/URLHelper';
/* Vue Routes */
const routes = [
@ -13,7 +14,7 @@ const routes = [
...authRoute.routes,
{
path: '/',
redirect: '/u/dashboard',
redirect: frontendURL('dashboard'),
},
];
@ -66,15 +67,15 @@ const redirectUser = (to, from, next) => {
const isAccessible =
window.roleWiseRoutes[currentUser.role].indexOf(to.name) > -1;
if (!isAccessible) {
return next('/u/dashboard');
return next(frontendURL('dashboard'));
}
}
// If unprotected and loggedIn -> redirect
if (unProtectedRoutes.indexOf(to.name) !== -1 && isLoggedIn) {
return next('/u/dashboard');
return next(frontendURL('dashboard'));
}
if (unProtectedRoutes.indexOf(to.name) === -1 && !isLoggedIn) {
return next('/u/login');
return next(frontendURL('login'));
}
return next();
};
@ -82,7 +83,7 @@ const redirectUser = (to, from, next) => {
// protecting routes
router.beforeEach((to, from, next) => {
if (!to.name) {
return next('/u/dashboard');
return next(frontendURL('dashboard'));
}
return redirectUser(to, from, next);

View file

@ -1,9 +1,10 @@
import Login from './Login';
import { frontendURL } from '../../helper/URLHelper';
export default {
routes: [
{
path: '/u/login',
path: frontendURL('login'),
name: 'login',
component: Login,
},

View file

@ -7,6 +7,7 @@ import defaultState from '../../i18n/default-sidebar';
import * as types from '../mutation-types';
import Account from '../../api/account';
import ChannelApi from '../../api/channels';
import { frontendURL } from '../../helper/URLHelper';
const state = defaultState;
// inboxes fetch flag
@ -135,7 +136,7 @@ const mutations = {
payload = payload.map(item => ({
channel_id: item.id,
label: item.name,
toState: `/u/inbox/${item.id}`,
toState: frontendURL(`inbox/${item.id}`),
channelType: item.channelType,
avatarUrl: item.avatar_url,
pageId: item.page_id,
@ -154,7 +155,7 @@ const mutations = {
menuItems.inbox.children.push({
channel_id: data.id,
label: data.name,
toState: `/u/inbox/${data.id}`,
toState: frontendURL(`inbox/${data.id}`),
channelType: data.channelType,
avatarUrl: data.avatar_url === undefined ? null : data.avatar_url,
pageId: data.page_id,

View file

@ -1,4 +1,7 @@
class ApplicationMailer < ActionMailer::Base
default from: 'accounts@chatwoot.com'
layout 'mailer'
# helpers
helper :frontend_urls
end

View file

@ -2,4 +2,4 @@
<p>You can confirm your account email through the link below:</p>
<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>
<p><%= link_to 'Confirm my account', frontend_url('auth/confirmation', confirmation_token: @token) %></p>

View file

@ -2,7 +2,7 @@
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
<p><%= link_to 'Change my password', frontend_url('auth/password/edit', reset_password_token: @token) %></p>
<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>

View file

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Chatwoot</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<% if current_user %>
<body data-account-id="<%= current_user.account_id %>" >
<% end %>
<% if user_signed_in? %>
<li>
<%= link_to('Logout', destroy_user_session_path, :method => :delete) %>
</li>
<% else %>
<li>
<%= link_to('Login', new_user_session_path) %>
</li>
<% end %>
<%= yield %>
</body>
</html>

View file

@ -20,6 +20,7 @@ Rails.application.configure do
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{1.hour.to_i}"
}
config.action_mailer.default_url_options = { :host => 'localhost', port: 3000 }
# Show full error reports and disable caching.
config.consider_all_requests_local = true

View file

@ -5,11 +5,13 @@ Rails.application.routes.draw do
mount_devise_token_auth_for 'User', at: 'auth', controllers: { confirmations: 'confirmations', passwords: 'passwords',
sessions: 'sessions' }, via: [:get, :post]
get "/u", to: "dashboard#index"
get "/u/*params", to: "dashboard#index"
get '/', to: redirect('/u/login')
match '/status', to: 'home#status', via: [:get] #for elb checks
root :to => "dashboard#index"
get "/app", to: "dashboard#index"
get "/app/*params", to: "dashboard#index"
match '/status', to: 'home#status', via: [:get]
namespace :api do
namespace :v1 do

View file

@ -18,6 +18,7 @@
"md5": "~2.2.1",
"moment": "~2.19.3",
"pusher-js": "~4.0.0",
"query-string": "5",
"spinkit": "~1.2.5",
"sweet-modal-vue": "~1.0.3",
"tween.js": "~16.6.0",

View file

@ -0,0 +1,17 @@
require "rails_helper"
describe FrontendUrlsHelper, type: :helper do
describe "#frontend_url" do
context "without query params" do
it "creates path correctly" do
expect(helper.frontend_url('dashboard')).to eq "http://test.host/app/dashboard"
end
end
context "with query params" do
it "creates path correctly" do
expect(helper.frontend_url('dashboard', p1: 'p1', p2: 'p2')).to eq "http://test.host/app/dashboard?p1=p1&p2=p2"
end
end
end
end

60
spec/rails_helper.rb Normal file
View file

@ -0,0 +1,60 @@
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
# Prevent database truncation if the environment is production
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!
# Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
# run as spec files by default. This means that files in spec/support that end
# in _spec.rb will both be required and run as specs, causing the specs to be
# run twice. It is recommended that you do not name files matching this glob to
# end with _spec.rb. You can configure this pattern with the --pattern
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
#
# The following line is provided for convenience purposes. It has the downside
# of increasing the boot-up time by auto-requiring all files in the support
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
# Checks for pending migrations and applies them before tests are run.
# If you are not using ActiveRecord, you can remove these lines.
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
puts e.to_s.strip
exit 1
end
RSpec.configure do |config|
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures"
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
config.use_transactional_fixtures = true
# RSpec Rails can automatically mix in different behaviours to your tests
# based on their file location, for example enabling you to call `get` and
# `post` in specs under `spec/controllers`.
#
# You can disable this behaviour by removing the line below, and instead
# explicitly tag your specs with their type, e.g.:
#
# RSpec.describe UsersController, :type => :controller do
# # ...
# end
#
# The different available types are documented in the features, such as in
# https://relishapp.com/rspec/rspec-rails/docs
config.infer_spec_type_from_file_location!
# Filter lines from Rails gems in backtraces.
config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
end

15
spec/spec_helper.rb Normal file
View file

@ -0,0 +1,15 @@
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
# config.include Rails.application.routes.url_helpers
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class Api::BaseControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class Api::V1::AgentsControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class Api::V1::CannedResponsesControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class Api::V1::ConversationsControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class Api::V1::ReportsControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class Api::V1::SubscriptionsControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class Api::V1::WebhooksControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class Api::V1::Widget::MessagesControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class HomeControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end

View file

View file

@ -1,7 +0,0 @@
require 'test_helper'
class AccountTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class AttachmentTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class CannedResponseTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class ChannelTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class ContactTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class ConversationTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class FacebookPageTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class InboxMemberTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class InboxTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class MessageTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class SubscriptionTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class TelegramBotTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -1,7 +0,0 @@
require 'test_helper'
class UserTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end

View file

@ -8422,6 +8422,15 @@ qs@~6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
query-string@5:
version "5.1.1"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==
dependencies:
decode-uri-component "^0.2.0"
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
query-string@^4.1.0:
version "4.3.4"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"