feat: CSAT response collection public page (#2685)

This commit is contained in:
Muhsin Keloth 2021-08-03 18:22:50 +05:30 committed by GitHub
parent 9b01b82cc7
commit 92c14fa87d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 371 additions and 10 deletions

View file

@ -25,7 +25,7 @@ Style/FrozenStringLiteralComment:
Style/SymbolArray:
Enabled: false
Style/OptionalBooleanParameter:
Exclude:
Exclude:
- 'app/services/email_templates/db_resolver_service.rb'
- 'app/dispatchers/dispatcher.rb'
Style/GlobalVars:
@ -57,6 +57,7 @@ Rails/ApplicationController:
- 'app/controllers/widgets_controller.rb'
- 'app/controllers/platform_controller.rb'
- 'app/controllers/public_controller.rb'
- 'app/controllers/survey/responses_controller.rb'
Style/ClassAndModuleChildren:
EnforcedStyle: compact
Exclude:
@ -79,7 +80,7 @@ Style/GuardClause:
- 'app/models/message.rb'
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
Metrics/AbcSize:
Exclude:
Exclude:
- 'app/controllers/concerns/auth_helper.rb'
- 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb'
- 'db/migrate/20161123131628_devise_token_auth_create_users.rb'
@ -108,7 +109,7 @@ Rails/BulkChangeTable:
- 'db/migrate/20191027054756_create_contact_inboxes.rb'
- 'db/migrate/20191130164019_add_template_type_to_messages.rb'
- 'db/migrate/20210425093724_convert_integration_hook_settings_field.rb'
Rails/UniqueValidationWithoutIndex:
Rails/UniqueValidationWithoutIndex:
Exclude:
- 'app/models/channel/twitter_profile.rb'
- 'app/models/webhook.rb'

View file

@ -0,0 +1,10 @@
class Survey::ResponsesController < ActionController::Base
before_action :set_global_config
def show; end
private
def set_global_config
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
end
end

View file

@ -0,0 +1,25 @@
import Vue from 'vue';
import Vuelidate from 'vuelidate';
import VueI18n from 'vue-i18n';
import App from '../survey/App.vue';
import i18n from '../survey/i18n';
Vue.use(VueI18n);
Vue.use(Vuelidate);
const i18nConfig = new VueI18n({
locale: 'en',
messages: i18n,
});
// Event Bus
window.bus = new Vue();
Vue.config.productionTip = false;
window.onload = () => {
window.WOOT_SURVEY = new Vue({
i18n: i18nConfig,
render: h => h(App),
}).$mount('#app');
};

View file

@ -16,21 +16,28 @@
</template>
<script>
import { mapGetters } from 'vuex';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import { BUS_EVENTS } from 'shared/constants/busEvents';
const {
LOGO_THUMBNAIL: logoThumbnail,
BRAND_NAME: brandName,
WIDGET_BRAND_URL: widgetBrandURL,
} = window.globalConfig || {};
export default {
mixins: [globalConfigMixin],
data() {
return {
referrerHost: '',
globalConfig: {
brandName,
logoThumbnail,
widgetBrandURL,
},
};
},
computed: {
...mapGetters({
globalConfig: 'globalConfig/get',
}),
brandRedirectURL() {
const baseURL = `${this.globalConfig.widgetBrandURL}?utm_source=widget_branding`;
if (this.referrerHost) {
@ -47,7 +54,6 @@ export default {
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';

View file

@ -1,5 +1,10 @@
<template>
<button :class="buttonClassName" :style="buttonStyles" @click="onClick">
<button
:class="buttonClassName"
:style="buttonStyles"
:disabled="disabled"
@click="onClick"
>
<slot></slot>
</button>
</template>
@ -22,6 +27,10 @@ export default {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
},
computed: {
buttonClassName() {

View file

@ -0,0 +1,74 @@
<template>
<label class="block">
<div
v-if="label"
class="mb-2 text-xs font-medium"
:class="{
'text-black-800': !error,
'text-red-400': error,
}"
>
{{ label }}
</div>
<textarea
class="
resize-none
border
rounded
w-full
py-2
px-3
text-slate-700
leading-tight
outline-none
"
:class="{
'border-black-200 hover:border-black-300 focus:border-black-300':
!error,
'border-red-200 hover:border-red-300 focus:border-red-300': error,
}"
:placeholder="placeholder"
:value="value"
@change="onChange"
/>
<div v-if="error" class="text-red-400 mt-2 text-xs font-medium">
{{ error }}
</div>
</label>
</template>
<script>
export default {
props: {
label: {
type: String,
default: '',
},
type: {
type: String,
default: 'text',
},
placeholder: {
type: String,
default: '',
},
value: {
type: [String, Number],
required: true,
},
error: {
type: String,
default: '',
},
},
methods: {
onChange(event) {
this.$emit('input', event.target.value);
},
},
};
</script>
<style lang="scss" scoped>
textarea {
min-height: 8rem;
}
</style>

20
app/javascript/survey/App.vue Executable file
View file

@ -0,0 +1,20 @@
<template>
<div id="app" class="woot-survey-wrap">
<response />
</div>
</template>
<script>
import Response from './views/Response.vue';
export default {
name: 'App',
components: {
Response,
},
};
</script>
<style lang="scss">
@import '~survey/assets/scss/woot.scss';
</style>

View file

@ -0,0 +1,21 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import 'widget/assets/scss/reset';
@import 'widget/assets/scss/variables';
@import 'widget/assets/scss/buttons';
@import 'widget/assets/scss/mixins';
@import 'widget/assets/scss/forms';
@import 'shared/assets/fonts/widget_fonts';
html,
body {
font-family: $font-family;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
height: 100%;
}
.woot-survey-wrap {
height: 100%;
}

View file

@ -0,0 +1,73 @@
<template>
<div class="customer-satisfcation mb-2">
<div class="ratings flex py-5 px-0">
<button
v-for="rating in ratings"
:key="rating.key"
:class="buttonClass(rating)"
@click="selectRating(rating)"
>
{{ rating.emoji }}
</button>
</div>
</div>
</template>
<script>
import { CSAT_RATINGS } from 'shared/constants/messages';
export default {
props: {
messageContentAttributes: {
type: Object,
default: () => {},
},
},
data() {
return {
email: '',
ratings: CSAT_RATINGS,
selectedRating: null,
};
},
computed: {
isRatingSubmitted() {
return this.messageContentAttributes?.csat_survey_response?.rating;
},
},
methods: {
buttonClass(rating) {
return [
{ selected: rating.value === this.selectedRating },
{ disabled: this.isRatingSubmitted },
{ hover: this.isRatingSubmitted },
'emoji-button shadow-none text-4xl outline-none mr-8',
];
},
selectRating(rating) {
this.selectedRating = rating.value;
},
},
};
</script>
<style lang="scss" scoped>
.emoji-button {
filter: grayscale(100%);
&.selected,
&:hover,
&:focus,
&:active {
filter: grayscale(0%);
transform: scale(1.32);
transition: transform 300ms;
}
&.disabled {
cursor: default;
opacity: 0.5;
pointer-events: none;
}
}
</style>

View file

@ -0,0 +1,5 @@
import { default as en } from './locale/en.json';
export default {
en,
};

View file

@ -0,0 +1,15 @@
{
"SURVEY": {
"DESCRIPTION": "Dear customer 👋 , please take a few moments to complete the feedback about the conversation.",
"RATING": {
"LABEL": "Rate your conversation",
"SUCCESS_MESSAGE": "Thank you for submitting the rating"
},
"FEEDBACK": {
"LABEL": "Do you have any thoughts you'd like to share?",
"PLACEHOLDER": "Your feedback (optional)",
"BUTTON_TEXT": "Submit feedback"
}
},
"POWERED_BY": "Powered by Chatwoot"
}

View file

@ -0,0 +1,64 @@
<template>
<div
class="w-full h-full flex flex-col flex-no-wrap overflow-hidden bg-white"
>
<div class="flex flex-1 overflow-auto">
<div class="max-w-screen-sm w-full my-0 m-auto px-8 py-12">
<img src="/brand-assets/logo.svg" alt="Chatwoot logo" class="logo" />
<p class="text-black-700 text-lg leading-relaxed mt-4 mb-4">
{{ $t('SURVEY.DESCRIPTION') }}
</p>
<label class="text-base font-medium text-black-800">
{{ $t('SURVEY.RATING.LABEL') }}
</label>
<rating />
<label class="text-base font-medium text-black-800">
{{ $t('SURVEY.FEEDBACK.LABEL') }}
</label>
<text-area
v-model="message"
class="my-5"
:placeholder="$t('SURVEY.FEEDBACK.PLACEHOLDER')"
/>
<custom-button class="font-medium float-right">
<spinner v-if="isSubmitted" class="p-0" />
{{ $t('SURVEY.FEEDBACK.BUTTON_TEXT') }}
</custom-button>
</div>
</div>
<div class="footer-wrap flex-shrink-0 w-full flex flex-col relative">
<branding></branding>
</div>
</div>
</template>
<script>
import Branding from 'shared/components/Branding.vue';
import Rating from 'survey/components/Rating.vue';
import CustomButton from 'shared/components/Button';
import TextArea from 'shared/components/TextArea.vue';
import configMixin from 'shared/mixins/configMixin';
export default {
name: 'Home',
components: {
Branding,
Rating,
CustomButton,
TextArea,
},
mixins: [configMixin],
data() {
return {
message: '',
isSubmitted: false,
};
},
};
</script>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
.logo {
max-height: $space-large;
}
</style>

View file

@ -69,7 +69,7 @@
</template>
<script>
import Branding from 'widget/components/Branding.vue';
import Branding from 'shared/components/Branding.vue';
import ChatFooter from 'widget/components/ChatFooter.vue';
import ChatHeaderExpanded from 'widget/components/ChatHeaderExpanded.vue';
import ChatHeader from 'widget/components/ChatHeader.vue';

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<title><%= @global_config['INSTALLATION_NAME'] %></title>
<%= csrf_meta_tags %>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<script>
window.globalConfig = <%= raw @global_config.to_json %>
</script>
<%= javascript_pack_tag 'survey' %>
<%= stylesheet_pack_tag 'survey' %>
</head>
<body>
<div id="app"></div>
<%= yield %>
</body>
</html>

View file

@ -20,6 +20,9 @@ Rails.application.routes.draw do
get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_twitter_inbox_agents'
resource :widget, only: [:show]
namespace :survey do
resources :responses, only: [:show]
end
end
get '/api', to: 'api#index'

View file

@ -6,6 +6,7 @@ const resolve = {
vue$: 'vue/dist/vue.common.js',
dashboard: path.resolve('./app/javascript/dashboard'),
widget: path.resolve('./app/javascript/widget'),
survey: path.resolve('./app/javascript/survey'),
assets: path.resolve('./app/javascript/dashboard/assets'),
components: path.resolve('./app/javascript/dashboard/components'),
'./iconfont.eot': 'vue-easytable/libs/font/iconfont.eot',

View file

@ -0,0 +1,16 @@
require 'rails_helper'
describe '/survey/response', type: :request do
describe 'GET survey/responses/{uuid}' do
it 'renders the page correctly when called' do
conversation = create(:conversation)
get survey_response_url(id: conversation.uuid)
expect(response).to be_successful
end
it 'returns 404 when called with invalid conversation uuid' do
get survey_response_url(id: '')
expect(response.status).to eq(404)
end
end
end

View file

@ -4,6 +4,7 @@ module.exports = {
purge: [
'./app/javascript/widget/**/*.vue',
'./app/javascript/shared/**/*.vue',
'./app/javascript/survey/**/*.vue',
],
future: {
removeDeprecatedGapUtilities: true,