[Enhancement] Add CopyToClipboard action in code component (#329)

* Add CopyToClipboard action in websiteWidgetCode component

* Fix codeclimate issues
This commit is contained in:
Pranav Raj S 2019-11-30 17:33:42 +05:30 committed by Sojan Jose
parent a3662091c7
commit 60e96f446e
18 changed files with 283 additions and 90 deletions

View file

@ -17,6 +17,9 @@ Metrics/BlockLength:
- spec/**/* - spec/**/*
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
EnforcedStyle: compact EnforcedStyle: compact
RSpec/NestedGroups:
Enabled: true
Max: 4
AllCops: AllCops:
Exclude: Exclude:
- db/* - db/*

View file

@ -1,17 +1,22 @@
class Api::V1::Widget::InboxesController < ApplicationController class Api::V1::Widget::InboxesController < Api::BaseController
before_action :authorize_request
def create def create
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
channel = web_widgets.create!( channel = web_widgets.create!(
website_name: permitted_params[:website_name], website_name: permitted_params[:website_name],
website_url: permitted_params[:website_url] website_url: permitted_params[:website_url]
) )
inbox = inboxes.create!(name: permitted_params[:website_name], channel: channel) @inbox = inboxes.create!(name: permitted_params[:website_name], channel: channel)
render json: inbox
end end
end end
private private
def authorize_request
authorize ::Inbox
end
def inboxes def inboxes
current_account.inboxes current_account.inboxes
end end

View file

@ -2,10 +2,10 @@
padding: $space-jumbo $space-smaller; padding: $space-jumbo $space-smaller;
.message { .message {
display: block;
width: 100%;
text-align: center;
color: $color-gray; color: $color-gray;
display: block;
text-align: center;
width: 100%;
} }
.spinner { .spinner {
@ -31,9 +31,9 @@
} }
.message { .message {
width: 50%;
margin: 0 auto;
color: $color-gray; color: $color-gray;
margin: $space-normal auto;
width: 90%;
} }
.button { .button {

View file

@ -0,0 +1,48 @@
<template>
<div class="code--container">
<button class="button small button--copy-code" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</button>
<highlight-code :lang="lang">
{{ script }}
</highlight-code>
</div>
</template>
<script>
/* global bus */
import 'highlight.js/styles/default.css';
import copy from 'copy-text-to-clipboard';
export default {
props: {
script: {
type: String,
required: true,
},
lang: {
type: String,
default: 'javascript',
},
},
methods: {
onCopy() {
copy(this.script);
bus.$emit('newToastMessage', this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
},
},
};
</script>
<style lang="scss" scoped>
.code--container {
position: relative;
text-align: left;
.button--copy-code {
margin-top: 0;
position: absolute;
right: 0;
}
}
</style>

View file

@ -1,26 +1,28 @@
/* eslint no-plusplus: 0 */ /* eslint no-plusplus: 0 */
/* eslint-env browser */ /* eslint-env browser */
import Bar from './widgets/chart/BarChart';
import Code from './Code';
import LoadingState from './widgets/LoadingState';
import Modal from './Modal'; import Modal from './Modal';
import ModalHeader from './ModalHeader';
import ReportStatsCard from './widgets/ReportStatsCard';
import Spinner from './Spinner'; import Spinner from './Spinner';
import SubmitButton from './buttons/FormSubmitButton'; import SubmitButton from './buttons/FormSubmitButton';
import Tabs from './ui/Tabs/Tabs'; import Tabs from './ui/Tabs/Tabs';
import TabsItem from './ui/Tabs/TabsItem'; import TabsItem from './ui/Tabs/TabsItem';
import LoadingState from './widgets/LoadingState';
import ReportStatsCard from './widgets/ReportStatsCard';
import Bar from './widgets/chart/BarChart';
import ModalHeader from './ModalHeader';
const WootUIKit = { const WootUIKit = {
Bar,
Code,
LoadingState,
Modal, Modal,
ModalHeader,
ReportStatsCard,
Spinner, Spinner,
SubmitButton, SubmitButton,
Tabs, Tabs,
TabsItem, TabsItem,
LoadingState,
ReportStatsCard,
Bar,
ModalHeader,
install(Vue) { install(Vue) {
const keys = Object.keys(this); const keys = Object.keys(this);
keys.pop(); // remove 'install' from keys keys.pop(); // remove 'install' from keys

View file

@ -0,0 +1,42 @@
export const createWebsiteWidgetScript = websiteToken => `
<script>
(function(d,t) {
var BASE_URL = '${window.location.origin}';
var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src= BASE_URL + "/packs/js/sdk.js";
s.parentNode.insertBefore(g,s);
g.onload=function(){
window.chatwootSDK.run({
websiteToken: '${websiteToken}',
baseUrl: BASE_URL
})
}
})(document,"script");
</script>
`;
export const createMessengerScript = pageId => `
<script>
window.fbAsyncInit = function() {
FB.init({
appId: "${window.chatwootConfig.fbAppId}",
xfbml: true,
version: "v4.0"
});
};
(function(d, s, id){
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) { return; }
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_US/sdk.js";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<div class="fb-messengermessageus"
messenger_app_id="${window.chatwootConfig.fbAppId}"
page_id="${pageId}"
color="blue"
size="standard" >
</div>
`;

View file

@ -6,6 +6,12 @@ export default {
TRIAL_MESSAGE: 'days trial remaining.', TRIAL_MESSAGE: 'days trial remaining.',
TRAIL_BUTTON: 'Buy Now', TRAIL_BUTTON: 'Buy Now',
}, },
COMPONENTS: {
CODE: {
BUTTON_TEXT: 'Copy',
COPY_SUCCESSFUL: 'Code copied to clipboard successfully',
},
},
CONFIRM_EMAIL: 'Verifying...', CONFIRM_EMAIL: 'Verifying...',
SETTINGS: { SETTINGS: {
INBOXES: { INBOXES: {

View file

@ -30,7 +30,7 @@
}, },
"AUTH": { "AUTH": {
"TITLE": "Channels", "TITLE": "Channels",
"DESC": "Currently we support only Facebook Pages as a platform. We have more platforms like Twitter, Telegram and Line in the works, which will be out soon." "DESC": "Currently we support website live chat widgets and Facebook Pages as platforms. We have more platforms like Twitter, Telegram and Line in the works, which will be out soon."
}, },
"AGENTS": { "AGENTS": {
"TITLE": "Agents", "TITLE": "Agents",
@ -58,7 +58,8 @@
"FINISH": { "FINISH": {
"TITLE": "Your Inbox is ready!", "TITLE": "Your Inbox is ready!",
"MESSAGE": "You can now engage with your customers through your new Channel. Happy supporting ", "MESSAGE": "You can now engage with your customers through your new Channel. Happy supporting ",
"BUTTON_TEXT": "Take me there" "BUTTON_TEXT": "Take me there",
"WEBSITE_SUCCESS": "You have successfully finished creating a website channel. Copy the code shown below and paste it on your website. Next time a customer use the live chat, the conversation will automatically appear on your inbox."
}, },
"REAUTH": "Reauthorize", "REAUTH": "Reauthorize",
"VIEW": "View", "VIEW": "View",
@ -71,7 +72,7 @@
"NO": "No, Keep " "NO": "No, Keep "
}, },
"API": { "API": {
"SUCCESS_MESSAGE": "Inbox delete successfully", "SUCCESS_MESSAGE": "Inbox deleted successfully",
"ERROR_MESSAGE": "Could not delete inbox. Please try again later." "ERROR_MESSAGE": "Could not delete inbox. Please try again later."
} }
}, },

View file

@ -1,7 +1,8 @@
<template> <template>
<div class="wizard-body columns content-box small-9"> <div class="wizard-body columns content-box small-9">
<loading-state :message="emptyStateMessage" v-if="showLoader"></loading-state> <loading-state v-if="showLoader" :message="emptyStateMessage">
<form class="row" v-on:submit.prevent="addAgents()" v-if="!showLoader"> </loading-state>
<form v-if="!showLoader" class="row" @submit.prevent="addAgents()">
<div class="medium-12 columns"> <div class="medium-12 columns">
<page-header <page-header
:header-title="$t('INBOX_MGMT.ADD.AGENTS.TITLE')" :header-title="$t('INBOX_MGMT.ADD.AGENTS.TITLE')"
@ -10,13 +11,28 @@
</div> </div>
<div class="medium-7 columns"> <div class="medium-7 columns">
<div class="medium-12 columns"> <div class="medium-12 columns">
<label :class="{ 'error': $v.selectedAgents.$error }">Agents <label :class="{ error: $v.selectedAgents.$error }">
<multiselect v-model="selectedAgents" :options="agentList" track-by="id" label="name" :multiple="true" :close-on-select="false" :clear-on-select="false" :hide-selected="true" placeholder="Pick some" @select="$v.selectedAgents.$touch"></multiselect> Agents
<span class="message" v-if="$v.selectedAgents.$error">Add atleast one agent to your new Inbox</span> <multiselect
v-model="selectedAgents"
:options="agentList"
track-by="id"
label="name"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:hide-selected="true"
placeholder="Pick some"
@select="$v.selectedAgents.$touch"
>
</multiselect>
<span v-if="$v.selectedAgents.$error" class="message">
Add atleast one agent to your new Inbox
</span>
</label> </label>
</div> </div>
<div class="medium-12 columns text-right"> <div class="medium-12 columns text-right">
<input type="submit" value="Create Inbox" class="button"> <input type="submit" value="Create Inbox" class="button" />
</div> </div>
</div> </div>
</form> </form>
@ -28,16 +44,13 @@
/* global bus */ /* global bus */
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import ChannelItem from '../../../../components/widgets/ChannelItem';
import ChannelApi from '../../../../api/channels'; import ChannelApi from '../../../../api/channels';
import router from '../../../index'; import router from '../../../index';
import PageHeader from '../SettingsSubPageHeader'; import PageHeader from '../SettingsSubPageHeader';
import LoadingState from '../../../../components/widgets/LoadingState'; import LoadingState from '../../../../components/widgets/LoadingState';
export default { export default {
components: { components: {
ChannelItem,
PageHeader, PageHeader,
LoadingState, LoadingState,
}, },
@ -75,14 +88,20 @@ export default {
ChannelApi.addAgentsToChannel(inboxId, this.selectedAgents.map(x => x.id)) ChannelApi.addAgentsToChannel(inboxId, this.selectedAgents.map(x => x.id))
.then(() => { .then(() => {
this.isCreating = false; this.isCreating = false;
router.replace({ name: 'settings_inbox_finish', params: { page: 'new', inbox_id: this.$route.params.inbox_id } }); router.replace({
}).catch((error) => { name: 'settings_inbox_finish',
params: {
page: 'new',
inbox_id: this.$route.params.inbox_id,
website_token: this.$route.params.website_token,
},
});
})
.catch(error => {
bus.$emit('newToastMessage', error.message); bus.$emit('newToastMessage', error.message);
this.isCreating = false; this.isCreating = false;
}); });
}, },
}, },
}; };
</script> </script>

View file

@ -1,19 +1,55 @@
<template> <template>
<div class="wizard-body columns content-box small-9"> <div class="wizard-body columns content-box small-9">
<empty-state :title="$t('INBOX_MGMT.FINISH.TITLE')" :message="$t('INBOX_MGMT.FINISH.MESSAGE')" :buttonText="$t('INBOX_MGMT.FINISH.BUTTON_TEXT')"> <empty-state
:title="$t('INBOX_MGMT.FINISH.TITLE')"
:message="message"
:button-text="$t('INBOX_MGMT.FINISH.BUTTON_TEXT')"
>
<div class="medium-12 columns text-center"> <div class="medium-12 columns text-center">
<router-link class="button success nice" :to="{ name: 'inbox_dashboard', params: { inboxId: this.$route.params.inbox_id }}">{{$t('INBOX_MGMT.FINISH.BUTTON_TEXT')}}</router-link> <div class="website--code">
<woot-code v-if="$route.params.website_token" :script="websiteScript">
</woot-code>
</div>
<router-link
class="button success nice"
:to="{
name: 'inbox_dashboard',
params: { inboxId: this.$route.params.inbox_id },
}"
>
{{ $t('INBOX_MGMT.FINISH.BUTTON_TEXT') }}
</router-link>
</div> </div>
</empty-state> </empty-state>
</div> </div>
</template> </template>
<script> <script>
import { createWebsiteWidgetScript } from 'dashboard/helper/scriptGenerator';
import EmptyState from '../../../../components/widgets/EmptyState'; import EmptyState from '../../../../components/widgets/EmptyState';
export default { export default {
components: { components: {
EmptyState, EmptyState,
}, },
computed: {
message() {
if (!this.$route.params.website_token) {
return this.$t('INBOX_MGMT.FINISH.MESSAGE');
}
return this.$t('INBOX_MGMT.FINISH.WEBSITE_SUCCESS');
},
websiteScript() {
return createWebsiteWidgetScript(this.$route.params.website_token);
},
},
}; };
</script> </script>
<style lang="scss" scoped>
@import '~dashboard/assets/scss/variables';
.website--code {
margin: $space-normal auto;
max-width: 60%;
}
</style>

View file

@ -15,11 +15,7 @@
<p class="sub-head"> <p class="sub-head">
{{ $t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD') }} {{ $t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD') }}
</p> </p>
<p class="code"> <woot-code :script="messengerScript"></woot-code>
<code>
{{ messengerScript }}
</code>
</p>
</div> </div>
<div <div
v-else-if="inbox.channelType === 'Channel::WebWidget'" v-else-if="inbox.channelType === 'Channel::WebWidget'"
@ -31,9 +27,7 @@
<p class="sub-head"> <p class="sub-head">
{{ $t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD') }} {{ $t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD') }}
</p> </p>
<highlight-code lang="javascript"> <woot-code :script="webWidgetScript"></woot-code>
{{ webWidgetScript }}
</highlight-code>
</div> </div>
<div class="agent-wrapper"> <div class="agent-wrapper">
<p class="title"> <p class="title">
@ -70,7 +64,10 @@
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
/* global bus */ /* global bus */
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import 'highlight.js/styles/default.css'; import {
createWebsiteWidgetScript,
createMessengerScript,
} from 'dashboard/helper/scriptGenerator';
export default { export default {
props: ['onClose', 'inbox', 'show'], props: ['onClose', 'inbox', 'show'],
@ -78,49 +75,18 @@ export default {
return { return {
selectedAgents: [], selectedAgents: [],
isUpdating: false, isUpdating: false,
messengerScript: `<script>
window.fbAsyncInit = function() {
FB.init({
appId: "${window.chatwootConfig.fbAppId}",
xfbml: true,
version: "v4.0"
});
};
(function(d, s, id){
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) { return; }
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_US/sdk.js";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
<\/script>
<div class="fb-messengermessageus"
messenger_app_id="${window.chatwootConfig.fbAppId}"
page_id="${this.inbox.pageId}"
color="blue"
size="standard" >
</div>`,
webWidgetScript: `
(function(d,t) {
var BASE_URL = '${window.location.origin}';
var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src= BASE_URL + "/packs/js/sdk.js";
s.parentNode.insertBefore(g,s);
g.onload=function(){
window.chatwootSDK.run({
websiteToken: '${this.inbox.websiteToken}',
baseUrl: BASE_URL
})
}
})(document,"script");
`,
}; };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
agentList: 'getAgents', agentList: 'getAgents',
}), }),
webWidgetScript() {
return createWebsiteWidgetScript(this.inbox.websiteToken);
},
messengerScript() {
return createMessengerScript(this.inbox.pageId);
},
}, },
mounted() { mounted() {
this.$store.dispatch('fetchAgents').then(() => { this.$store.dispatch('fetchAgents').then(() => {

View file

@ -4,10 +4,11 @@
:header-title="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.TITLE')" :header-title="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.TITLE')"
:header-content="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.DESC')" :header-content="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.DESC')"
/> />
<loading-state <woot-loading-state
v-if="isCreating" v-if="isCreating"
message="Creating Website Support Channel" message="Creating Website Support Channel"
></loading-state> >
</woot-loading-state>
<form v-if="!isCreating" class="row" @submit.prevent="createChannel()"> <form v-if="!isCreating" class="row" @submit.prevent="createChannel()">
<div class="medium-12 columns"> <div class="medium-12 columns">
<label> <label>
@ -61,10 +62,14 @@ export default {
}; };
}, },
mounted() { mounted() {
bus.$on('new_website_channel', ({ inboxId }) => { bus.$on('new_website_channel', ({ inboxId, websiteToken }) => {
router.replace({ router.replace({
name: 'settings_inboxes_add_agents', name: 'settings_inboxes_add_agents',
params: { page: 'new', inbox_id: inboxId }, params: {
page: 'new',
inbox_id: inboxId,
website_token: websiteToken,
},
}); });
}); });
}, },

View file

@ -71,7 +71,10 @@ const actions = {
try { try {
const response = await WebChannel.create(params); const response = await WebChannel.create(params);
commit(types.default.SET_INBOX_ITEM, response); commit(types.default.SET_INBOX_ITEM, response);
bus.$emit('new_website_channel', { inboxId: response.data.id }); bus.$emit('new_website_channel', {
inboxId: response.data.id,
websiteToken: response.data.website_token,
});
} catch (error) { } catch (error) {
// Handle error // Handle error
} }

View file

@ -20,6 +20,10 @@ class InboxPolicy < ApplicationPolicy
true true
end end
def create?
@user.administrator?
end
def destroy? def destroy?
@user.administrator? @user.administrator?
end end

View file

@ -0,0 +1,5 @@
json.id @inbox.id
json.channel_id @inbox.channel_id
json.name @inbox.name
json.channel_type @inbox.channel_type
json.website_token @inbox.channel.try(:website_token)

View file

@ -20,6 +20,7 @@
"babel-plugin-transform-vue-jsx": "^3.7.0", "babel-plugin-transform-vue-jsx": "^3.7.0",
"bourbon": "^6.0.0", "bourbon": "^6.0.0",
"chart.js": "~2.5.0", "chart.js": "~2.5.0",
"copy-text-to-clipboard": "^2.1.1",
"dotenv": "^8.0.0", "dotenv": "^8.0.0",
"emojione": "~2.2.7", "emojione": "~2.2.7",
"foundation-sites": "~6.5.3", "foundation-sites": "~6.5.3",

View file

@ -0,0 +1,42 @@
require 'rails_helper'
RSpec.describe '/api/v1/widget/inboxes', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:params) { { website: { website_name: 'test', website_url: 'test.com' } } }
describe 'POST /api/v1/widget/inboxes' do
context 'when unauthenticated user' do
it 'returns unauthorized' do
post '/api/v1/widget/inboxes', params: params
expect(response).to have_http_status(:unauthorized)
end
end
context 'when user is logged in' do
context 'with user as administrator' do
it 'creates inbox and returns website_token' do
post '/api/v1/widget/inboxes', params: params, headers: admin.create_new_auth_token, as: :json
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['name']).to eq('test')
expect(json_response['website_token']).not_to be_empty
end
end
context 'with user as agent' do
it 'returns unauthorized' do
post '/api/v1/widget/inboxes',
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
end
end

View file

@ -2764,6 +2764,11 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
copy-text-to-clipboard@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/copy-text-to-clipboard/-/copy-text-to-clipboard-2.1.1.tgz#5340e8620976d2dd9de0ff11493d13a80d600fd2"
integrity sha512-oSuMj4ArDGSLcLPsDhzWOhalzOVV0ErCHNfZNNr+spC+iWJ6PVSLzPPrJw/rcdFZyOhugn8iw6O0nrpY/ZrEMg==
core-js-compat@^3.1.1: core-js-compat@^3.1.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.2.1.tgz#0cbdbc2e386e8e00d3b85dc81c848effec5b8150" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.2.1.tgz#0cbdbc2e386e8e00d3b85dc81c848effec5b8150"