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 name: eslint
command: yarn run eslint command: yarn run eslint
- run: # - run:
name: brakeman # name: brakeman
command: 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 # Store yarn / webpacker cache
- save_cache: - save_cache:

View file

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

1
.rspec Normal file
View file

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

View file

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

View file

@ -172,6 +172,7 @@ GEM
crass (1.0.4) crass (1.0.4)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
diff-lcs (1.3)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
dotenv (0.7.0) dotenv (0.7.0)
@ -329,6 +330,23 @@ GEM
http-cookie (>= 1.0.2, < 2.0) http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0) mime-types (>= 1.16, < 4.0)
netrc (~> 0.8) 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) rubocop (0.74.0)
jaro_winkler (~> 1.5.1) jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
@ -456,6 +474,7 @@ DEPENDENCIES
redis-rack-cache redis-rack-cache
responders responders
rest-client rest-client
rspec-rails (~> 3.8)
rubocop (~> 0.74.0) rubocop (~> 0.74.0)
sass-rails (~> 5.0) sass-rails (~> 5.0)
seed_dump seed_dump

View file

@ -28,6 +28,6 @@ class ConfirmationsController < Devise::ConfirmationsController
user.reset_password_token = enc user.reset_password_token = enc
user.reset_password_sent_at = Time.now.utc user.reset_password_sent_at = Time.now.utc
user.save(validate: false) 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
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 moment from 'moment';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import endPoints from './endPoints'; import endPoints from './endPoints';
import { frontendURL } from '../helper/URLHelper';
export default { export default {
login(creds) { login(creds) {
@ -65,7 +66,7 @@ export default {
if (error.response.status === 401) { if (error.response.status === 401) {
Cookies.remove('auth_data'); Cookies.remove('auth_data');
Cookies.remove('user'); Cookies.remove('user');
window.location = '/login'; window.location = frontendURL('login');
} }
reject(error); reject(error);
}); });
@ -80,7 +81,7 @@ export default {
.then(response => { .then(response => {
Cookies.remove('auth_data'); Cookies.remove('auth_data');
Cookies.remove('user'); Cookies.remove('user');
window.location = '/u/login'; window.location = frontendURL('login');
resolve(response); resolve(response);
}) })
.catch(error => { .catch(error => {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,34 @@
<template> <template>
<div class="conversation" :class="{ active: isActiveChat, 'unread-chat': hasUnread }" @click="cardClick(chat)" > <div
<Thumbnail :src="chat.meta.sender.thumbnail" :badge="chat.meta.sender.channel" class="columns" /> 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"> <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> <h4 class="conversation--user">
<p class="conversation--message" v-html="extractMessageText(lastMessage(chat))"></p> {{ 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"> <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> <span class="unread">{{ getUnreadCount }}</span>
</div> </div>
</div> </div>
@ -14,27 +37,25 @@
<script> <script>
/* eslint no-console: 0 */ /* eslint no-console: 0 */
/* eslint no-extra-boolean-cast: 0 */ /* eslint no-extra-boolean-cast: 0 */
/* global bus */
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import Thumbnail from '../Thumbnail'; import Thumbnail from '../Thumbnail';
import getEmojiSVG from '../emoji/utils'; import getEmojiSVG from '../emoji/utils';
import conversationMixin from '../../../mixins/conversations'; import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time'; import timeMixin from '../../../mixins/time';
import router from '../../../routes'; import router from '../../../routes';
import { frontendURL } from '../../../helper/URLHelper';
export default { export default {
props: [
'chat',
],
mixins: [timeMixin, conversationMixin],
components: { components: {
Thumbnail, Thumbnail,
}, },
created() { mixins: [timeMixin, conversationMixin],
// console.log(this.chat.inbox_id); props: {
chat: {
type: Object,
default: () => {},
},
}, },
computed: { computed: {
@ -64,7 +85,7 @@ export default {
methods: { methods: {
cardClick(chat) { cardClick(chat) {
router.push({ router.push({
path: `/u/conversations/${chat.id}`, path: frontendURL(`conversations/${chat.id}`),
}); });
}, },
extractMessageText(chatItem) { extractMessageText(chatItem) {
@ -85,7 +106,9 @@ export default {
}, },
getEmojiSVG, getEmojiSVG,
inboxName(inboxId) { 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; 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 { export default {
menuGroup: { menuGroup: {
common: { common: {
@ -14,7 +16,7 @@ export default {
label: 'Conversations', label: 'Conversations',
hasSubMenu: false, hasSubMenu: false,
key: '', key: '',
toState: '/u/dashboard', toState: frontendURL('dashboard'),
toolTip: 'Conversation from all subscribed inboxes', toolTip: 'Conversation from all subscribed inboxes',
toStateName: 'home', toStateName: 'home',
}, },
@ -22,14 +24,14 @@ export default {
icon: 'ion-arrow-graph-up-right', icon: 'ion-arrow-graph-up-right',
label: 'Reports', label: 'Reports',
hasSubMenu: false, hasSubMenu: false,
toState: '/u/reports', toState: frontendURL('reports'),
toStateName: 'settings_account_reports', toStateName: 'settings_account_reports',
}, },
settings: { settings: {
icon: 'ion-settings', icon: 'ion-settings',
label: 'Settings', label: 'Settings',
hasSubMenu: false, hasSubMenu: false,
toState: '/u/settings/', toState: frontendURL('settings'),
toStateName: 'settings_home', toStateName: 'settings_home',
}, },
inbox: { inbox: {
@ -39,7 +41,7 @@ export default {
newLink: true, newLink: true,
key: 'inbox', key: 'inbox',
cssClass: 'menu-title align-justify', cssClass: 'menu-title align-justify',
toState: '/u/settings/inboxes', toState: frontendURL('settings/inboxes'),
toStateName: 'settings_inbox_list', toStateName: 'settings_inbox_list',
children: [], children: [],
}, },
@ -65,41 +67,41 @@ export default {
label: 'Home', label: 'Home',
hasSubMenu: false, hasSubMenu: false,
toStateName: 'home', toStateName: 'home',
toState: '/u/dashboard', toState: frontendURL('dashboard'),
}, },
agents: { agents: {
icon: 'ion-person-stalker', icon: 'ion-person-stalker',
label: 'Agents', label: 'Agents',
hasSubMenu: false, hasSubMenu: false,
toState: '/u/settings/agents/list', toState: frontendURL('settings/agents/list'),
toStateName: 'agent_list', toStateName: 'agent_list',
}, },
inboxes: { inboxes: {
icon: 'ion-archive', icon: 'ion-archive',
label: 'Inboxes', label: 'Inboxes',
hasSubMenu: false, hasSubMenu: false,
toState: '/u/settings/inboxes/list', toState: frontendURL('settings/inboxes/list'),
toStateName: 'settings_inbox_list', toStateName: 'settings_inbox_list',
}, },
cannedResponses: { cannedResponses: {
icon: 'ion-chatbox-working', icon: 'ion-chatbox-working',
label: 'Canned Responses', label: 'Canned Responses',
hasSubMenu: false, hasSubMenu: false,
toState: '/u/settings/canned-response/list', toState: frontendURL('settings/canned-response/list'),
toStateName: 'canned_list', toStateName: 'canned_list',
}, },
billing: { billing: {
icon: 'ion-card', icon: 'ion-card',
label: 'Billing', label: 'Billing',
hasSubMenu: false, hasSubMenu: false,
toState: '/u/settings/billing', toState: frontendURL('settings/billing'),
toStateName: 'billing', toStateName: 'billing',
}, },
account: { account: {
icon: 'ion-beer', icon: 'ion-beer',
label: 'Account Settings', label: 'Account Settings',
hasSubMenu: false, hasSubMenu: false,
toState: '/u/settings/account', toState: frontendURL('settings/account'),
toStateName: 'account', toStateName: 'account',
}, },
}, },

View file

@ -1,7 +1,7 @@
{ {
"INBOX_MGMT": { "INBOX_MGMT": {
"HEADER": "Inboxes", "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": { "LIST": {
"404": "There are no inboxes attached to this account." "404": "There are no inboxes attached to this account."
}, },

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="column content-box"> <div class="column content-box">
<button <button
class="button icon success btn-fixed-right-top" class="button nice icon success btn-fixed-right-top"
@click="openAddPopup()" @click="openAddPopup()"
> >
<i class="icon ion-android-add-circle"></i> <i class="icon ion-android-add-circle"></i>
@ -10,20 +10,35 @@
<!-- List Canned Response --> <!-- List Canned Response -->
<div class="row"> <div class="row">
<div class="small-8 columns"> <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') }} {{ $t('CANNED_MGMT.LIST.404') }}
</p> </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> <thead>
<!-- Header --> <!-- 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 }} {{ thHeader }}
</th> </th>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(cannedItem, index) in cannedResponseList"> <tr
v-for="(cannedItem, index) in cannedResponseList"
:key="cannedItem.short_code"
>
<!-- Short Code --> <!-- Short Code -->
<td>{{ cannedItem.short_code }}</td> <td>{{ cannedItem.short_code }}</td>
<!-- Content --> <!-- Content -->
@ -63,11 +78,11 @@
<!-- Edit Canned Response --> <!-- Edit Canned Response -->
<woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup"> <woot-modal :show.sync="showEditPopup" :on-close="hideEditPopup">
<edit-canned <edit-canned
v-if="showEditPopup"
:id="selectedResponse.id"
:edshort-code="selectedResponse.short_code" :edshort-code="selectedResponse.short_code"
:edcontent="selectedResponse.content" :edcontent="selectedResponse.content"
:id="selectedResponse.id"
:on-close="hideEditPopup" :on-close="hideEditPopup"
v-if="showEditPopup"
/> />
</woot-modal> </woot-modal>
@ -82,13 +97,11 @@
:reject-text="deleteRejectText" :reject-text="deleteRejectText"
/> />
</div> </div>
</template> </template>
<script> <script>
/* global bus */ /* global bus */
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import PageHeader from '../SettingsSubPageHeader';
import AddCanned from './AddCanned'; import AddCanned from './AddCanned';
import EditCanned from './EditCanned'; import EditCanned from './EditCanned';
import DeleteCanned from './DeleteCanned'; import DeleteCanned from './DeleteCanned';
@ -96,7 +109,6 @@ import DeleteCanned from './DeleteCanned';
export default { export default {
components: { components: {
AddCanned, AddCanned,
PageHeader,
EditCanned, EditCanned,
DeleteCanned, DeleteCanned,
}, },
@ -119,13 +131,19 @@ export default {
}), }),
// Delete Modal // Delete Modal
deleteConfirmText() { 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() { 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() { 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() { mounted() {
@ -173,11 +191,14 @@ export default {
this.deleteCannedResponse(this.selectedResponse.id); this.deleteCannedResponse(this.selectedResponse.id);
}, },
deleteCannedResponse(id) { deleteCannedResponse(id) {
this.$store.dispatch('deleteCannedResponse', { this.$store
.dispatch('deleteCannedResponse', {
id, id,
}).then(() => { })
.then(() => {
this.showAlert(this.$t('CANNED_MGMT.DELETE.API.SUCCESS_MESSAGE')); this.showAlert(this.$t('CANNED_MGMT.DELETE.API.SUCCESS_MESSAGE'));
}).catch(() => { })
.catch(() => {
this.showAlert(this.$t('CANNED_MGMT.DELETE.API.ERROR_MESSAGE')); this.showAlert(this.$t('CANNED_MGMT.DELETE.API.ERROR_MESSAGE'));
}); });
}, },

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,4 +2,4 @@
<p>You can confirm your account email through the link below:</p> <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>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>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> <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 = { config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{1.hour.to_i}" '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. # Show full error reports and disable caching.
config.consider_all_requests_local = true 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', mount_devise_token_auth_for 'User', at: 'auth', controllers: { confirmations: 'confirmations', passwords: 'passwords',
sessions: 'sessions' }, via: [:get, :post] sessions: 'sessions' }, via: [:get, :post]
get "/u", to: "dashboard#index"
get "/u/*params", to: "dashboard#index"
get '/', to: redirect('/u/login') root :to => "dashboard#index"
match '/status', to: 'home#status', via: [:get] #for elb checks
get "/app", to: "dashboard#index"
get "/app/*params", to: "dashboard#index"
match '/status', to: 'home#status', via: [:get]
namespace :api do namespace :api do
namespace :v1 do namespace :v1 do

View file

@ -18,6 +18,7 @@
"md5": "~2.2.1", "md5": "~2.2.1",
"moment": "~2.19.3", "moment": "~2.19.3",
"pusher-js": "~4.0.0", "pusher-js": "~4.0.0",
"query-string": "5",
"spinkit": "~1.2.5", "spinkit": "~1.2.5",
"sweet-modal-vue": "~1.0.3", "sweet-modal-vue": "~1.0.3",
"tween.js": "~16.6.0", "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" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== 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: query-string@^4.1.0:
version "4.3.4" version "4.3.4"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"