Merge branch 'develop' into feat/new-auth-screens
This commit is contained in:
commit
449b29197a
117 changed files with 1357 additions and 358 deletions
72
.github/workflows/run-foss-spec.yml
vendored
Normal file
72
.github/workflows/run-foss-spec.yml
vendored
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
# #
|
||||||
|
# # This action will strip the enterprise folder
|
||||||
|
# # and run the spec.
|
||||||
|
# # This is set to run against every PR.
|
||||||
|
# #
|
||||||
|
|
||||||
|
name: Run Chatwoot CE spec
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:10.8
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ""
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
# needed because the postgres container does not provide a healthcheck
|
||||||
|
# tmpfs makes DB faster by using RAM
|
||||||
|
options: >-
|
||||||
|
--mount type=tmpfs,destination=/var/lib/postgresql/data
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
ports:
|
||||||
|
- 6379:6379
|
||||||
|
options: --entrypoint redis-server
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 3.0.2 # Not needed with a .ruby-version file
|
||||||
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
|
- name: yarn
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Strip enterprise code
|
||||||
|
run: |
|
||||||
|
rm -rf enterprise
|
||||||
|
rm -rf spec/enterprise
|
||||||
|
|
||||||
|
- name: Create database
|
||||||
|
run: bundle exec rake db:create
|
||||||
|
|
||||||
|
- name: Seed database
|
||||||
|
run: bundle exec rake db:schema:load
|
||||||
|
|
||||||
|
- name: yarn check-files
|
||||||
|
run: yarn install --check-files
|
||||||
|
|
||||||
|
# Run rails tests
|
||||||
|
- name: Run backend tests
|
||||||
|
run: |
|
||||||
|
bundle exec rspec --profile=10 --format documentation
|
|
@ -403,7 +403,7 @@ GEM
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.6)
|
||||||
puma (5.6.2)
|
puma (5.6.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.2.0)
|
pundit (2.2.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
|
|
|
@ -45,7 +45,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def toggle_status
|
def toggle_status
|
||||||
head :not_found && return if conversation.nil?
|
return head :not_found if conversation.nil?
|
||||||
|
|
||||||
|
return head :forbidden unless @web_widget.end_conversation?
|
||||||
|
|
||||||
unless conversation.resolved?
|
unless conversation.resolved?
|
||||||
conversation.status = :resolved
|
conversation.status = :resolved
|
||||||
conversation.save
|
conversation.save
|
||||||
|
|
|
@ -3,6 +3,7 @@ class WidgetTestsController < ActionController::Base
|
||||||
before_action :ensure_widget_position
|
before_action :ensure_widget_position
|
||||||
before_action :ensure_widget_type
|
before_action :ensure_widget_type
|
||||||
before_action :ensure_widget_style
|
before_action :ensure_widget_style
|
||||||
|
before_action :ensure_dark_mode
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render
|
render
|
||||||
|
@ -14,6 +15,10 @@ class WidgetTestsController < ActionController::Base
|
||||||
@widget_style = params[:widget_style] || 'standard'
|
@widget_style = params[:widget_style] || 'standard'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_dark_mode
|
||||||
|
@dark_mode = params[:dark_mode] || 'light'
|
||||||
|
end
|
||||||
|
|
||||||
def ensure_widget_position
|
def ensure_widget_position
|
||||||
@widget_position = params[:position] || 'left'
|
@widget_position = params[:position] || 'left'
|
||||||
end
|
end
|
||||||
|
|
|
@ -57,3 +57,13 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
button:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: var(--w-100);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--w-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
{{ attribute.label }}
|
{{ attribute.label }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="filter__answer--wrap">
|
<div v-if="showActionInput" class="filter__answer--wrap">
|
||||||
<div v-if="inputType">
|
<div v-if="inputType">
|
||||||
<div
|
<div
|
||||||
v-if="inputType === 'multi_select'"
|
v-if="inputType === 'multi_select'"
|
||||||
|
@ -89,6 +89,10 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => null,
|
default: () => null,
|
||||||
},
|
},
|
||||||
|
showActionInput: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
action_name: {
|
action_name: {
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default {
|
||||||
background: var(--s-500);
|
background: var(--s-500);
|
||||||
}
|
}
|
||||||
&__busy {
|
&__busy {
|
||||||
background: var(--y-700);
|
background: var(--y-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -207,7 +207,11 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
this.formatMessage(this.data.content, this.isATweet) + botMessageContent
|
this.formatMessage(
|
||||||
|
this.data.content,
|
||||||
|
this.isATweet,
|
||||||
|
this.data.private
|
||||||
|
) + botMessageContent
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
contentAttributes() {
|
contentAttributes() {
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
|
const formatArray = params => {
|
||||||
|
if (params.length <= 0) {
|
||||||
|
params = [];
|
||||||
|
} else if (params.every(elem => typeof elem === 'string')) {
|
||||||
|
params = [...params];
|
||||||
|
} else {
|
||||||
|
params = params.map(val => val.id);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
};
|
||||||
|
|
||||||
const generatePayload = data => {
|
const generatePayload = data => {
|
||||||
const actions = JSON.parse(JSON.stringify(data));
|
const actions = JSON.parse(JSON.stringify(data));
|
||||||
let payload = actions.map(item => {
|
let payload = actions.map(item => {
|
||||||
if (Array.isArray(item.action_params)) {
|
if (Array.isArray(item.action_params)) {
|
||||||
item.action_params = item.action_params.map(val => val.id);
|
item.action_params = formatArray(item.action_params);
|
||||||
} else if (typeof item.values === 'object') {
|
} else if (typeof item.values === 'object') {
|
||||||
item.action_params = [item.action_params.id];
|
item.action_params = [item.action_params.id];
|
||||||
} else if (!item.action_params) {
|
} else if (!item.action_params) {
|
||||||
|
|
|
@ -395,7 +395,8 @@
|
||||||
"FEATURES": {
|
"FEATURES": {
|
||||||
"LABEL": "Features",
|
"LABEL": "Features",
|
||||||
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
|
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
|
||||||
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget"
|
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget",
|
||||||
|
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget"
|
||||||
},
|
},
|
||||||
"SETTINGS_POPUP": {
|
"SETTINGS_POPUP": {
|
||||||
"MESSENGER_HEADING": "Messenger Script",
|
"MESSENGER_HEADING": "Messenger Script",
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
"SUCCESS_MESSAGE": "Successfully changed the password",
|
"SUCCESS_MESSAGE": "Successfully changed the password",
|
||||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||||
},
|
},
|
||||||
|
"CAPTCHA": {
|
||||||
|
"ERROR": "Verification expired. Please solve captcha again."
|
||||||
|
},
|
||||||
"SUBMIT": "Submit"
|
"SUBMIT": "Submit"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,9 +78,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-if="globalConfig.hCaptchaSiteKey" class="h-captcha--box">
|
<div v-if="globalConfig.hCaptchaSiteKey" class="h-captcha--box">
|
||||||
<vue-hcaptcha
|
<vue-hcaptcha
|
||||||
|
ref="hCaptcha"
|
||||||
|
:class="{ error: !hasAValidCaptcha && didCaptchaReset }"
|
||||||
:sitekey="globalConfig.hCaptchaSiteKey"
|
:sitekey="globalConfig.hCaptchaSiteKey"
|
||||||
@verify="onRecaptchaVerified"
|
@verify="onRecaptchaVerified"
|
||||||
/>
|
/>
|
||||||
|
<span
|
||||||
|
v-if="!hasAValidCaptcha && didCaptchaReset"
|
||||||
|
class="captcha-error"
|
||||||
|
>
|
||||||
|
{{ $t('SET_NEW_PASSWORD.CAPTCHA.ERROR') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<auth-submit-button
|
<auth-submit-button
|
||||||
:label="$t('REGISTER.SUBMIT')"
|
:label="$t('REGISTER.SUBMIT')"
|
||||||
|
@ -137,6 +145,7 @@ export default {
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
hCaptchaClientResponse: '',
|
hCaptchaClientResponse: '',
|
||||||
},
|
},
|
||||||
|
didCaptchaReset: false,
|
||||||
isSignupInProgress: false,
|
isSignupInProgress: false,
|
||||||
error: '',
|
error: '',
|
||||||
};
|
};
|
||||||
|
@ -191,6 +200,7 @@ export default {
|
||||||
async submit() {
|
async submit() {
|
||||||
this.$v.$touch();
|
this.$v.$touch();
|
||||||
if (this.$v.$invalid) {
|
if (this.$v.$invalid) {
|
||||||
|
this.resetCaptcha();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isSignupInProgress = true;
|
this.isSignupInProgress = true;
|
||||||
|
@ -202,6 +212,7 @@ export default {
|
||||||
} 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) {
|
||||||
|
this.resetCaptcha();
|
||||||
errorMessage = error.response.data.message;
|
errorMessage = error.response.data.message;
|
||||||
}
|
}
|
||||||
this.showAlert(errorMessage);
|
this.showAlert(errorMessage);
|
||||||
|
@ -211,6 +222,15 @@ export default {
|
||||||
},
|
},
|
||||||
onRecaptchaVerified(token) {
|
onRecaptchaVerified(token) {
|
||||||
this.credentials.hCaptchaClientResponse = token;
|
this.credentials.hCaptchaClientResponse = token;
|
||||||
|
this.didCaptchaReset = false;
|
||||||
|
},
|
||||||
|
resetCaptcha() {
|
||||||
|
if (!this.globalConfig.hCaptchaSiteKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$refs.hCaptcha.reset();
|
||||||
|
this.credentials.hCaptchaClientResponse = '';
|
||||||
|
this.didCaptchaReset = true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -230,9 +250,19 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-captcha--box {
|
.h-captcha--box {
|
||||||
justify-content: center;
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: var(--space-one);
|
margin-bottom: var(--space-one);
|
||||||
|
|
||||||
|
.captcha-error {
|
||||||
|
color: var(--r-400);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::v-deep .error {
|
||||||
|
iframe {
|
||||||
|
border: 1px solid var(--r-500);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1200px) {
|
@media screen and (max-width: 1200px) {
|
||||||
|
|
|
@ -35,7 +35,9 @@
|
||||||
</td>
|
</td>
|
||||||
<!-- Agent Name + Email -->
|
<!-- Agent Name + Email -->
|
||||||
<td>
|
<td>
|
||||||
<span class="agent-name">{{ agent.name }}</span>
|
<span class="agent-name">
|
||||||
|
{{ agent.name }}
|
||||||
|
</span>
|
||||||
<span>{{ agent.email }}</span>
|
<span>{{ agent.email }}</span>
|
||||||
</td>
|
</td>
|
||||||
<!-- Agent Role + Verification Status -->
|
<!-- Agent Role + Verification Status -->
|
||||||
|
|
|
@ -100,6 +100,9 @@
|
||||||
:dropdown-values="
|
:dropdown-values="
|
||||||
getActionDropdownValues(automation.actions[i].action_name)
|
getActionDropdownValues(automation.actions[i].action_name)
|
||||||
"
|
"
|
||||||
|
:show-action-input="
|
||||||
|
showActionInput(automation.actions[i].action_name)
|
||||||
|
"
|
||||||
:v="$v.automation.actions.$each[i]"
|
:v="$v.automation.actions.$each[i]"
|
||||||
@resetAction="resetAction(i)"
|
@resetAction="resetAction(i)"
|
||||||
@removeAction="removeAction(i)"
|
@removeAction="removeAction(i)"
|
||||||
|
@ -413,14 +416,15 @@ export default {
|
||||||
submitAutomation() {
|
submitAutomation() {
|
||||||
this.$v.$touch();
|
this.$v.$touch();
|
||||||
if (this.$v.$invalid) return;
|
if (this.$v.$invalid) return;
|
||||||
this.automation.conditions[
|
const automation = JSON.parse(JSON.stringify(this.automation));
|
||||||
this.automation.conditions.length - 1
|
automation.conditions[
|
||||||
|
automation.conditions.length - 1
|
||||||
].query_operator = null;
|
].query_operator = null;
|
||||||
this.automation.conditions = filterQueryGenerator(
|
automation.conditions = filterQueryGenerator(
|
||||||
this.automation.conditions
|
automation.conditions
|
||||||
).payload;
|
).payload;
|
||||||
this.automation.actions = actionQueryGenerator(this.automation.actions);
|
automation.actions = actionQueryGenerator(automation.actions);
|
||||||
this.$emit('saveAutomation', this.automation);
|
this.$emit('saveAutomation', automation);
|
||||||
},
|
},
|
||||||
resetFilter(index, currentCondition) {
|
resetFilter(index, currentCondition) {
|
||||||
this.automation.conditions[index].filter_operator = this.automationTypes[
|
this.automation.conditions[index].filter_operator = this.automationTypes[
|
||||||
|
@ -438,6 +442,13 @@ export default {
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
showActionInput(actionName) {
|
||||||
|
const type = AUTOMATION_ACTION_TYPES.find(
|
||||||
|
action => action.key === actionName
|
||||||
|
).inputType;
|
||||||
|
if (type === null) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -97,6 +97,9 @@
|
||||||
:dropdown-values="
|
:dropdown-values="
|
||||||
getActionDropdownValues(automation.actions[i].action_name)
|
getActionDropdownValues(automation.actions[i].action_name)
|
||||||
"
|
"
|
||||||
|
:show-action-input="
|
||||||
|
showActionInput(automation.actions[i].action_name)
|
||||||
|
"
|
||||||
:v="$v.automation.actions.$each[i]"
|
:v="$v.automation.actions.$each[i]"
|
||||||
@removeAction="removeAction(i)"
|
@removeAction="removeAction(i)"
|
||||||
/>
|
/>
|
||||||
|
@ -192,7 +195,13 @@ export default {
|
||||||
required,
|
required,
|
||||||
$each: {
|
$each: {
|
||||||
action_params: {
|
action_params: {
|
||||||
required,
|
required: requiredIf(prop => {
|
||||||
|
return !(
|
||||||
|
prop.action_name === 'mute_conversation' ||
|
||||||
|
prop.action_name === 'snooze_convresation' ||
|
||||||
|
prop.action_name === 'resolve_convresation'
|
||||||
|
);
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -246,7 +255,7 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.formatConditions(this.selectedResponse);
|
this.formatAutomation(this.selectedResponse);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onEventChange() {
|
onEventChange() {
|
||||||
|
@ -414,14 +423,15 @@ export default {
|
||||||
submitAutomation() {
|
submitAutomation() {
|
||||||
this.$v.$touch();
|
this.$v.$touch();
|
||||||
if (this.$v.$invalid) return;
|
if (this.$v.$invalid) return;
|
||||||
this.automation.conditions[
|
const automation = JSON.parse(JSON.stringify(this.automation));
|
||||||
this.automation.conditions.length - 1
|
automation.conditions[
|
||||||
|
automation.conditions.length - 1
|
||||||
].query_operator = null;
|
].query_operator = null;
|
||||||
this.automation.conditions = filterQueryGenerator(
|
automation.conditions = filterQueryGenerator(
|
||||||
this.automation.conditions
|
automation.conditions
|
||||||
).payload;
|
).payload;
|
||||||
this.automation.actions = actionQueryGenerator(this.automation.actions);
|
automation.actions = actionQueryGenerator(automation.actions);
|
||||||
this.$emit('saveAutomation', this.automation, 'EDIT');
|
this.$emit('saveAutomation', automation, 'EDIT');
|
||||||
},
|
},
|
||||||
resetFilter(index, currentCondition) {
|
resetFilter(index, currentCondition) {
|
||||||
this.automation.conditions[index].filter_operator = this.automationTypes[
|
this.automation.conditions[index].filter_operator = this.automationTypes[
|
||||||
|
@ -436,7 +446,7 @@ export default {
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
formatConditions(automation) {
|
formatAutomation(automation) {
|
||||||
const formattedConditions = automation.conditions.map(condition => {
|
const formattedConditions = automation.conditions.map(condition => {
|
||||||
const inputType = this.automationTypes[
|
const inputType = this.automationTypes[
|
||||||
automation.event_name
|
automation.event_name
|
||||||
|
@ -456,11 +466,20 @@ export default {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const formattedActions = automation.actions.map(action => {
|
const formattedActions = automation.actions.map(action => {
|
||||||
|
let actionParams = [];
|
||||||
|
if (action.action_params.length) {
|
||||||
|
const inputType = AUTOMATION_ACTION_TYPES.find(
|
||||||
|
item => item.key === action.action_name
|
||||||
|
).inputType;
|
||||||
|
if (inputType === 'multi_select') {
|
||||||
|
actionParams = [
|
||||||
|
...this.getActionDropdownValues(action.action_name),
|
||||||
|
].filter(item => [...action.action_params].includes(item.id));
|
||||||
|
} else actionParams = [...action.action_params];
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...action,
|
...action,
|
||||||
action_params: [
|
action_params: actionParams,
|
||||||
...this.getActionDropdownValues(action.action_name),
|
|
||||||
].filter(item => [...action.action_params].includes(item.id)),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
this.automation = {
|
this.automation = {
|
||||||
|
@ -469,6 +488,13 @@ export default {
|
||||||
actions: formattedActions,
|
actions: formattedActions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
showActionInput(actionName) {
|
||||||
|
const type = AUTOMATION_ACTION_TYPES.find(
|
||||||
|
action => action.key === actionName
|
||||||
|
).inputType;
|
||||||
|
if (type === null) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -285,6 +285,17 @@
|
||||||
{{ $t('INBOX_MGMT.FEATURES.DISPLAY_EMOJI_PICKER') }}
|
{{ $t('INBOX_MGMT.FEATURES.DISPLAY_EMOJI_PICKER') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isAWebWidgetInbox" class="settings-item settings-item">
|
||||||
|
<input
|
||||||
|
v-model="selectedFeatureFlags"
|
||||||
|
type="checkbox"
|
||||||
|
value="end_conversation"
|
||||||
|
@input="handleFeatureFlag"
|
||||||
|
/>
|
||||||
|
<label for="end_conversation">
|
||||||
|
{{ $t('INBOX_MGMT.FEATURES.ALLOW_END_CONVERSATION') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<woot-submit-button
|
<woot-submit-button
|
||||||
v-if="isAPIInbox"
|
v-if="isAPIInbox"
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { IFrameHelper } from '../sdk/IFrameHelper';
|
import { IFrameHelper } from '../sdk/IFrameHelper';
|
||||||
import { getBubbleView } from '../sdk/settingsHelper';
|
import {
|
||||||
|
getBubbleView,
|
||||||
|
getDarkMode,
|
||||||
|
getWidgetStyle,
|
||||||
|
} from '../sdk/settingsHelper';
|
||||||
import {
|
import {
|
||||||
computeHashForUserData,
|
computeHashForUserData,
|
||||||
getUserCookieName,
|
getUserCookieName,
|
||||||
|
@ -24,8 +28,9 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
||||||
type: getBubbleView(chatwootSettings.type),
|
type: getBubbleView(chatwootSettings.type),
|
||||||
launcherTitle: chatwootSettings.launcherTitle || '',
|
launcherTitle: chatwootSettings.launcherTitle || '',
|
||||||
showPopoutButton: chatwootSettings.showPopoutButton || false,
|
showPopoutButton: chatwootSettings.showPopoutButton || false,
|
||||||
widgetStyle: chatwootSettings.widgetStyle || 'standard',
|
widgetStyle: getWidgetStyle(chatwootSettings.widgetStyle) || 'standard',
|
||||||
resetTriggered: false,
|
resetTriggered: false,
|
||||||
|
darkMode: getDarkMode(chatwootSettings.darkMode),
|
||||||
|
|
||||||
toggle(state) {
|
toggle(state) {
|
||||||
IFrameHelper.events.toggleBubble(state);
|
IFrameHelper.events.toggleBubble(state);
|
||||||
|
|
|
@ -145,6 +145,7 @@ export const IFrameHelper = {
|
||||||
hideMessageBubble: window.$chatwoot.hideMessageBubble,
|
hideMessageBubble: window.$chatwoot.hideMessageBubble,
|
||||||
showPopoutButton: window.$chatwoot.showPopoutButton,
|
showPopoutButton: window.$chatwoot.showPopoutButton,
|
||||||
widgetStyle: window.$chatwoot.widgetStyle,
|
widgetStyle: window.$chatwoot.widgetStyle,
|
||||||
|
darkMode: window.$chatwoot.darkMode,
|
||||||
});
|
});
|
||||||
IFrameHelper.onLoad({
|
IFrameHelper.onLoad({
|
||||||
widgetColor: message.config.channelConfig.widgetColor,
|
widgetColor: message.config.channelConfig.widgetColor,
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export const BUBBLE_DESIGN = ['standard', 'expanded_bubble'];
|
export const BUBBLE_DESIGN = ['standard', 'expanded_bubble'];
|
||||||
export const WIDGET_DESIGN = ['standard', 'flat'];
|
export const WIDGET_DESIGN = ['standard', 'flat'];
|
||||||
|
export const DARK_MODE = ['light', 'auto'];
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { BUBBLE_DESIGN, WIDGET_DESIGN } from './constants';
|
import { BUBBLE_DESIGN, DARK_MODE, WIDGET_DESIGN } from './constants';
|
||||||
|
|
||||||
export const getBubbleView = type =>
|
export const getBubbleView = type =>
|
||||||
BUBBLE_DESIGN.includes(type) ? type : BUBBLE_DESIGN[0];
|
BUBBLE_DESIGN.includes(type) ? type : BUBBLE_DESIGN[0];
|
||||||
|
@ -9,3 +9,6 @@ export const getWidgetStyle = style =>
|
||||||
WIDGET_DESIGN.includes(style) ? style : WIDGET_DESIGN[0];
|
WIDGET_DESIGN.includes(style) ? style : WIDGET_DESIGN[0];
|
||||||
|
|
||||||
export const isFlatWidgetStyle = style => style === 'flat';
|
export const isFlatWidgetStyle = style => style === 'flat';
|
||||||
|
|
||||||
|
export const getDarkMode = darkMode =>
|
||||||
|
DARK_MODE.includes(darkMode) ? darkMode : DARK_MODE[0];
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card-message chat-bubble agent">
|
<div
|
||||||
|
class="card-message chat-bubble agent"
|
||||||
|
:class="$dm('bg-white', 'dark:bg-slate-700')"
|
||||||
|
>
|
||||||
<img class="media" :src="mediaUrl" />
|
<img class="media" :src="mediaUrl" />
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="title">
|
<h4 class="title" :class="$dm('text-black-900', 'dark:text-slate-50')">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h4>
|
</h4>
|
||||||
<p class="body">
|
<p class="body" :class="$dm('text-black-700', 'dark:text-slate-100')">
|
||||||
{{ description }}
|
{{ description }}
|
||||||
</p>
|
</p>
|
||||||
<card-button
|
<card-button
|
||||||
|
@ -19,11 +22,13 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CardButton from 'shared/components/CardButton';
|
import CardButton from 'shared/components/CardButton';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
CardButton,
|
CardButton,
|
||||||
},
|
},
|
||||||
|
mixins: [darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -52,7 +57,6 @@ export default {
|
||||||
@import '~dashboard/assets/scss/mixins.scss';
|
@import '~dashboard/assets/scss/mixins.scss';
|
||||||
|
|
||||||
.card-message {
|
.card-message {
|
||||||
background: white;
|
|
||||||
max-width: 220px;
|
max-width: 220px;
|
||||||
padding: $space-small;
|
padding: $space-small;
|
||||||
border-radius: $space-small;
|
border-radius: $space-small;
|
||||||
|
@ -63,12 +67,10 @@ export default {
|
||||||
font-weight: $font-weight-medium;
|
font-weight: $font-weight-medium;
|
||||||
margin-top: $space-smaller;
|
margin-top: $space-smaller;
|
||||||
margin-bottom: $space-smaller;
|
margin-bottom: $space-smaller;
|
||||||
color: $color-heading;
|
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
color: $color-body;
|
|
||||||
margin-bottom: $space-smaller;
|
margin-bottom: $space-smaller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,10 +79,11 @@ export default {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button + .action-button {
|
.action-button + .action-button {
|
||||||
background: white;
|
background: $color-white;
|
||||||
@include thin-border($color-woot);
|
@include thin-border($color-woot);
|
||||||
color: $color-woot;
|
color: $color-woot;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="form chat-bubble agent">
|
<div
|
||||||
|
class="form chat-bubble agent"
|
||||||
|
:class="$dm('bg-white', 'dark:bg-slate-700')"
|
||||||
|
>
|
||||||
<form @submit.prevent="onSubmit">
|
<form @submit.prevent="onSubmit">
|
||||||
<div
|
<div
|
||||||
v-for="item in items"
|
v-for="item in items"
|
||||||
|
@ -9,10 +12,13 @@
|
||||||
'has-submitted': hasSubmitted,
|
'has-submitted': hasSubmitted,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<label>{{ item.label }}</label>
|
<label :class="$dm('text-black-900', 'dark:text-slate-50')">{{
|
||||||
|
item.label
|
||||||
|
}}</label>
|
||||||
<input
|
<input
|
||||||
v-if="item.type === 'email'"
|
v-if="item.type === 'email'"
|
||||||
v-model="formValues[item.name]"
|
v-model="formValues[item.name]"
|
||||||
|
:class="inputColor"
|
||||||
:type="item.type"
|
:type="item.type"
|
||||||
:pattern="item.regex"
|
:pattern="item.regex"
|
||||||
:title="item.title"
|
:title="item.title"
|
||||||
|
@ -24,6 +30,7 @@
|
||||||
<input
|
<input
|
||||||
v-else-if="item.type === 'text'"
|
v-else-if="item.type === 'text'"
|
||||||
v-model="formValues[item.name]"
|
v-model="formValues[item.name]"
|
||||||
|
:class="inputColor"
|
||||||
:required="item.required && 'required'"
|
:required="item.required && 'required'"
|
||||||
:pattern="item.pattern"
|
:pattern="item.pattern"
|
||||||
:title="item.title"
|
:title="item.title"
|
||||||
|
@ -35,6 +42,7 @@
|
||||||
<textarea
|
<textarea
|
||||||
v-else-if="item.type === 'text_area'"
|
v-else-if="item.type === 'text_area'"
|
||||||
v-model="formValues[item.name]"
|
v-model="formValues[item.name]"
|
||||||
|
:class="inputColor"
|
||||||
:required="item.required && 'required'"
|
:required="item.required && 'required'"
|
||||||
:title="item.title"
|
:title="item.title"
|
||||||
:name="item.name"
|
:name="item.name"
|
||||||
|
@ -44,6 +52,7 @@
|
||||||
<select
|
<select
|
||||||
v-else-if="item.type === 'select'"
|
v-else-if="item.type === 'select'"
|
||||||
v-model="formValues[item.name]"
|
v-model="formValues[item.name]"
|
||||||
|
:class="inputColor"
|
||||||
:required="item.required && 'required'"
|
:required="item.required && 'required'"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
|
@ -73,7 +82,10 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
buttonLabel: {
|
buttonLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -98,6 +110,10 @@ export default {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
widgetColor: 'appConfig/getWidgetColor',
|
widgetColor: 'appConfig/getWidgetColor',
|
||||||
}),
|
}),
|
||||||
|
inputColor() {
|
||||||
|
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
|
||||||
|
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
|
||||||
|
},
|
||||||
isFormValid() {
|
isFormValid() {
|
||||||
return this.items.reduce((acc, { name }) => {
|
return this.items.reduce((acc, { name }) => {
|
||||||
return !!this.formValues[name] && acc;
|
return !!this.formValues[name] && acc;
|
||||||
|
@ -186,7 +202,6 @@ export default {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: 1px solid $color-border;
|
border: 1px solid $color-border;
|
||||||
border-radius: $space-smaller;
|
border-radius: $space-smaller;
|
||||||
background-color: $color-white;
|
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: $space-normal;
|
font-size: $space-normal;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {},
|
components: {},
|
||||||
props: {
|
props: {
|
||||||
|
@ -51,7 +52,6 @@ export default {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: $space-large;
|
border-radius: $space-large;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: $color-woot;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: auto;
|
height: auto;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="options-message chat-bubble agent">
|
<div
|
||||||
|
class="options-message chat-bubble agent"
|
||||||
|
:class="$dm('bg-white', 'dark:bg-slate-700')"
|
||||||
|
>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="title">
|
<h4 class="title" :class="$dm('text-black-900', 'dark:text-slate-50')">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h4>
|
</h4>
|
||||||
<ul
|
<ul
|
||||||
|
@ -23,11 +26,13 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ChatOption from 'shared/components/ChatOption';
|
import ChatOption from 'shared/components/ChatOption';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ChatOption,
|
ChatOption,
|
||||||
},
|
},
|
||||||
|
mixins: [darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -80,7 +85,6 @@ export default {
|
||||||
font-weight: $font-weight-normal;
|
font-weight: $font-weight-normal;
|
||||||
margin-top: $space-smaller;
|
margin-top: $space-smaller;
|
||||||
margin-bottom: $space-smaller;
|
margin-bottom: $space-smaller;
|
||||||
color: $color-heading;
|
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="customer-satisfcation" :style="{ borderColor: widgetColor }">
|
<div
|
||||||
<h6 class="title">
|
class="customer-satisfaction"
|
||||||
|
:class="$dm('bg-white', 'dark:bg-slate-700')"
|
||||||
|
:style="{ borderColor: widgetColor }"
|
||||||
|
>
|
||||||
|
<h6 class="title" :class="$dm('text-slate-900', 'dark:text-slate-50')">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h6>
|
</h6>
|
||||||
<div class="ratings">
|
<div class="ratings">
|
||||||
|
@ -21,8 +25,9 @@
|
||||||
<input
|
<input
|
||||||
v-model="feedback"
|
v-model="feedback"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
|
:class="inputColor"
|
||||||
:placeholder="$t('CSAT.PLACEHOLDER')"
|
:placeholder="$t('CSAT.PLACEHOLDER')"
|
||||||
@keyup.enter="onSubmit"
|
@keydown.enter="onSubmit"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="button small"
|
class="button small"
|
||||||
|
@ -41,12 +46,14 @@ import { mapGetters } from 'vuex';
|
||||||
import Spinner from 'shared/components/Spinner';
|
import Spinner from 'shared/components/Spinner';
|
||||||
import { CSAT_RATINGS } from 'shared/constants/messages';
|
import { CSAT_RATINGS } from 'shared/constants/messages';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Spinner,
|
Spinner,
|
||||||
FluentIcon,
|
FluentIcon,
|
||||||
},
|
},
|
||||||
|
mixins: [darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
messageContentAttributes: {
|
messageContentAttributes: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -67,9 +74,7 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({ widgetColor: 'appConfig/getWidgetColor' }),
|
||||||
widgetColor: 'appConfig/getWidgetColor',
|
|
||||||
}),
|
|
||||||
isRatingSubmitted() {
|
isRatingSubmitted() {
|
||||||
return this.messageContentAttributes?.csat_survey_response?.rating;
|
return this.messageContentAttributes?.csat_survey_response?.rating;
|
||||||
},
|
},
|
||||||
|
@ -80,6 +85,10 @@ export default {
|
||||||
isButtonDisabled() {
|
isButtonDisabled() {
|
||||||
return !(this.selectedRating && this.feedback);
|
return !(this.selectedRating && this.feedback);
|
||||||
},
|
},
|
||||||
|
inputColor() {
|
||||||
|
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
|
||||||
|
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
|
||||||
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.isRatingSubmitted
|
return this.isRatingSubmitted
|
||||||
? this.$t('CSAT.SUBMITTED_TITLE')
|
? this.$t('CSAT.SUBMITTED_TITLE')
|
||||||
|
@ -136,10 +145,9 @@ export default {
|
||||||
@import '~widget/assets/scss/variables.scss';
|
@import '~widget/assets/scss/variables.scss';
|
||||||
@import '~widget/assets/scss/mixins.scss';
|
@import '~widget/assets/scss/mixins.scss';
|
||||||
|
|
||||||
.customer-satisfcation {
|
.customer-satisfaction {
|
||||||
@include light-shadow;
|
@include light-shadow;
|
||||||
|
|
||||||
background: $color-white;
|
|
||||||
border-bottom-left-radius: $space-smaller;
|
border-bottom-left-radius: $space-smaller;
|
||||||
border-radius: $space-small;
|
border-radius: $space-small;
|
||||||
border-top: $space-micro solid $color-woot;
|
border-top: $space-micro solid $color-woot;
|
||||||
|
@ -193,6 +201,10 @@ export default {
|
||||||
border-top: 1px solid $color-border;
|
border-top: 1px solid $color-border;
|
||||||
padding: $space-one;
|
padding: $space-one;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $color-light-gray;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="date--separator">
|
<div
|
||||||
|
class="date--separator"
|
||||||
|
:class="$dm('text-slate-700', 'dark:text-slate-200')"
|
||||||
|
>
|
||||||
{{ formattedDate }}
|
{{ formattedDate }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { formatDate } from 'shared/helpers/DateHelper';
|
import { formatDate } from 'shared/helpers/DateHelper';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
date: {
|
date: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -30,7 +36,6 @@ export default {
|
||||||
|
|
||||||
.date--separator {
|
.date--separator {
|
||||||
font-size: $font-size-default;
|
font-size: $font-size-default;
|
||||||
color: $color-body;
|
|
||||||
height: 50px;
|
height: 50px;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
placeholder: {
|
placeholder: {
|
||||||
|
|
|
@ -1,17 +1,52 @@
|
||||||
import { mount } from '@vue/test-utils';
|
|
||||||
import DateSeparator from '../DateSeparator';
|
import DateSeparator from '../DateSeparator';
|
||||||
|
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
import VueI18n from 'vue-i18n';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
const localVue = createLocalVue();
|
||||||
|
import i18n from 'dashboard/i18n';
|
||||||
|
localVue.use(Vuex);
|
||||||
|
localVue.use(VueI18n);
|
||||||
|
|
||||||
describe('DateSeparator', () => {
|
const i18nConfig = new VueI18n({
|
||||||
test('matches snapshot', () => {
|
locale: 'en',
|
||||||
const wrapper = mount(DateSeparator, {
|
messages: i18n,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dateSeparator', () => {
|
||||||
|
let store = null;
|
||||||
|
let actions = null;
|
||||||
|
let modules = null;
|
||||||
|
let dateSeparator = null;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
actions = {};
|
||||||
|
|
||||||
|
modules = {
|
||||||
|
auth: {
|
||||||
|
getters: {
|
||||||
|
'appConfig/darkMode': () => 'light',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
store = new Vuex.Store({
|
||||||
|
actions,
|
||||||
|
modules,
|
||||||
|
});
|
||||||
|
|
||||||
|
dateSeparator = shallowMount(DateSeparator, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
date: 'Nov 18, 2019',
|
date: 'Nov 18, 2019',
|
||||||
},
|
},
|
||||||
mocks: {
|
i18n: i18nConfig,
|
||||||
$t: () => {},
|
mixins: [darkModeMixin],
|
||||||
},
|
|
||||||
});
|
});
|
||||||
expect(wrapper.vm).toBeTruthy();
|
});
|
||||||
expect(wrapper.element).toMatchSnapshot();
|
|
||||||
|
it('date separator snapshot', () => {
|
||||||
|
expect(dateSeparator.vm).toBeTruthy();
|
||||||
|
expect(dateSeparator.element).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`DateSeparator matches snapshot 1`] = `
|
exports[`dateSeparator date separator snapshot 1`] = `
|
||||||
<div
|
<div
|
||||||
class="date--separator"
|
class="date--separator text-slate-700"
|
||||||
>
|
>
|
||||||
|
|
||||||
Nov 18, 2019
|
Nov 18, 2019
|
||||||
|
|
|
@ -13,8 +13,9 @@ const TWITTER_HASH_REPLACEMENT =
|
||||||
const USER_MENTIONS_REGEX = /mention:\/\/(user|team)\/(\d+)\/(.+)/gm;
|
const USER_MENTIONS_REGEX = /mention:\/\/(user|team)\/(\d+)\/(.+)/gm;
|
||||||
|
|
||||||
class MessageFormatter {
|
class MessageFormatter {
|
||||||
constructor(message, isATweet = false) {
|
constructor(message, isATweet = false, isAPrivateNote = false) {
|
||||||
this.message = DOMPurify.sanitize(escapeHtml(message || ''));
|
this.message = DOMPurify.sanitize(escapeHtml(message || ''));
|
||||||
|
this.isAPrivateNote = isAPrivateNote;
|
||||||
this.isATweet = isATweet;
|
this.isATweet = isATweet;
|
||||||
this.marked = marked;
|
this.marked = marked;
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ class MessageFormatter {
|
||||||
}
|
}
|
||||||
|
|
||||||
formatMessage() {
|
formatMessage() {
|
||||||
if (this.isATweet) {
|
if (this.isATweet && !this.isAPrivateNote) {
|
||||||
const withUserName = this.message.replace(
|
const withUserName = this.message.replace(
|
||||||
TWITTER_USERNAME_REGEX,
|
TWITTER_USERNAME_REGEX,
|
||||||
TWITTER_USERNAME_REPLACEMENT
|
TWITTER_USERNAME_REPLACEMENT
|
||||||
|
|
|
@ -36,19 +36,45 @@ describe('#MessageFormatter', () => {
|
||||||
it('should add links to @mentions', () => {
|
it('should add links to @mentions', () => {
|
||||||
const message =
|
const message =
|
||||||
'@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername';
|
'@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername';
|
||||||
expect(new MessageFormatter(message, true).formattedMessage).toMatch(
|
expect(
|
||||||
|
new MessageFormatter(message, true, false).formattedMessage
|
||||||
|
).toMatch(
|
||||||
'<p><a href="http://twitter.com/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">@chatwootapp</a> is an opensource tool thanks @longnonexistenttwitterusername</p>'
|
'<p><a href="http://twitter.com/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">@chatwootapp</a> is an opensource tool thanks @longnonexistenttwitterusername</p>'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add links to #tags', () => {
|
it('should add links to #tags', () => {
|
||||||
const message = '#chatwootapp is an opensource tool';
|
const message = '#chatwootapp is an opensource tool';
|
||||||
expect(new MessageFormatter(message, true).formattedMessage).toMatch(
|
expect(
|
||||||
|
new MessageFormatter(message, true, false).formattedMessage
|
||||||
|
).toMatch(
|
||||||
'<p><a href="https://twitter.com/hashtag/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">#chatwootapp</a> is an opensource tool</p>'
|
'<p><a href="https://twitter.com/hashtag/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">#chatwootapp</a> is an opensource tool</p>'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('private notes', () => {
|
||||||
|
it('should return the same string if not tags or @mentions', () => {
|
||||||
|
const message = 'Chatwoot is an opensource tool';
|
||||||
|
expect(new MessageFormatter(message).formattedMessage).toMatch(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add links to @mentions', () => {
|
||||||
|
const message =
|
||||||
|
'@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername';
|
||||||
|
expect(
|
||||||
|
new MessageFormatter(message, false, true).formattedMessage
|
||||||
|
).toMatch(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add links to #tags', () => {
|
||||||
|
const message = '#chatwootapp is an opensource tool';
|
||||||
|
expect(
|
||||||
|
new MessageFormatter(message, false, true).formattedMessage
|
||||||
|
).toMatch(message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('plain text content', () => {
|
describe('plain text content', () => {
|
||||||
it('returns the plain text without HTML', () => {
|
it('returns the plain text without HTML', () => {
|
||||||
const message =
|
const message =
|
||||||
|
|
|
@ -3,8 +3,12 @@ import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
methods: {
|
methods: {
|
||||||
formatMessage(message, isATweet) {
|
formatMessage(message, isATweet, isAPrivateNote) {
|
||||||
const messageFormatter = new MessageFormatter(message, isATweet);
|
const messageFormatter = new MessageFormatter(
|
||||||
|
message,
|
||||||
|
isATweet,
|
||||||
|
isAPrivateNote
|
||||||
|
);
|
||||||
return messageFormatter.formattedMessage;
|
return messageFormatter.formattedMessage;
|
||||||
},
|
},
|
||||||
getPlainText(message, isATweet) {
|
getPlainText(message, isATweet) {
|
||||||
|
|
|
@ -15,5 +15,5 @@
|
||||||
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
|
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"POWERED_BY": "مدعوم بواسطة تشات وت"
|
"POWERED_BY": "مدعوم بواسطة Chatwoot"
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,7 +138,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
registerUnreadEvents() {
|
registerUnreadEvents() {
|
||||||
bus.$on(ON_AGENT_MESSAGE_RECEIVED, this.setUnreadView);
|
bus.$on(ON_AGENT_MESSAGE_RECEIVED, () => {
|
||||||
|
const { name: routeName } = this.$route;
|
||||||
|
if (this.isWidgetOpen && routeName === 'messages') {
|
||||||
|
this.$store.dispatch('conversation/setUserLastSeen');
|
||||||
|
}
|
||||||
|
this.setUnreadView();
|
||||||
|
});
|
||||||
bus.$on(ON_UNREAD_MESSAGE_CLICK, () => {
|
bus.$on(ON_UNREAD_MESSAGE_CLICK, () => {
|
||||||
this.replaceRoute('messages').then(() => this.unsetUnreadView());
|
this.replaceRoute('messages').then(() => this.unsetUnreadView());
|
||||||
});
|
});
|
||||||
|
@ -175,6 +181,7 @@ export default {
|
||||||
},
|
},
|
||||||
setUnreadView() {
|
setUnreadView() {
|
||||||
const { unreadMessageCount } = this;
|
const { unreadMessageCount } = this;
|
||||||
|
|
||||||
if (this.isIFrame && unreadMessageCount > 0 && !this.isWidgetOpen) {
|
if (this.isIFrame && unreadMessageCount > 0 && !this.isWidgetOpen) {
|
||||||
this.replaceRoute('unread-messages').then(() => {
|
this.replaceRoute('unread-messages').then(() => {
|
||||||
this.setIframeHeight(true);
|
this.setIframeHeight(true);
|
||||||
|
|
|
@ -42,7 +42,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-name {
|
.agent-name {
|
||||||
color: $color-body;
|
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
font-weight: $font-weight-medium;
|
font-weight: $font-weight-medium;
|
||||||
margin: $space-small 0;
|
margin: $space-small 0;
|
||||||
|
@ -210,7 +209,6 @@
|
||||||
|
|
||||||
.chat-bubble {
|
.chat-bubble {
|
||||||
@include light-shadow;
|
@include light-shadow;
|
||||||
background: $color-woot;
|
|
||||||
border-radius: $space-two;
|
border-radius: $space-two;
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -242,7 +240,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.agent {
|
&.agent {
|
||||||
background: $color-white;
|
|
||||||
border-bottom-left-radius: $space-smaller;
|
border-bottom-left-radius: $space-smaller;
|
||||||
color: $color-body;
|
color: $color-body;
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<div
|
<div
|
||||||
v-if="hasAttachments"
|
v-if="hasAttachments"
|
||||||
class="chat-bubble has-attachment agent"
|
class="chat-bubble has-attachment agent"
|
||||||
:class="wrapClass"
|
:class="(wrapClass, $dm('bg-white', 'dark:bg-slate-50'))"
|
||||||
>
|
>
|
||||||
<div v-for="attachment in message.attachments" :key="attachment.id">
|
<div v-for="attachment in message.attachments" :key="attachment.id">
|
||||||
<image-bubble
|
<image-bubble
|
||||||
|
@ -40,7 +40,11 @@
|
||||||
<file-bubble v-else :url="attachment.data_url" />
|
<file-bubble v-else :url="attachment.data_url" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="message.showAvatar || hasRecordedResponse" class="agent-name">
|
<p
|
||||||
|
v-if="message.showAvatar || hasRecordedResponse"
|
||||||
|
class="agent-name"
|
||||||
|
:class="$dm('text-slate-700', 'dark:text-slate-200')"
|
||||||
|
>
|
||||||
{{ agentName }}
|
{{ agentName }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -68,6 +72,8 @@ import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||||
import configMixin from '../mixins/configMixin';
|
import configMixin from '../mixins/configMixin';
|
||||||
import messageMixin from '../mixins/messageMixin';
|
import messageMixin from '../mixins/messageMixin';
|
||||||
import { isASubmittedFormMessage } from 'shared/helpers/MessageTypeHelper';
|
import { isASubmittedFormMessage } from 'shared/helpers/MessageTypeHelper';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AgentMessage',
|
name: 'AgentMessage',
|
||||||
components: {
|
components: {
|
||||||
|
@ -77,7 +83,7 @@ export default {
|
||||||
UserMessage,
|
UserMessage,
|
||||||
FileBubble,
|
FileBubble,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, configMixin, messageMixin],
|
mixins: [timeMixin, configMixin, messageMixin, darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
message: {
|
message: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
|
@ -5,8 +5,13 @@
|
||||||
!isCards && !isOptions && !isForm && !isArticle && !isCards && !isCSAT
|
!isCards && !isOptions && !isForm && !isArticle && !isCards && !isCSAT
|
||||||
"
|
"
|
||||||
class="chat-bubble agent"
|
class="chat-bubble agent"
|
||||||
|
:class="$dm('bg-white', 'dark:bg-slate-700')"
|
||||||
>
|
>
|
||||||
<div class="message-content" v-html="formatMessage(message, false)"></div>
|
<div
|
||||||
|
class="message-content"
|
||||||
|
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
||||||
|
v-html="formatMessage(message, false)"
|
||||||
|
></div>
|
||||||
<email-input
|
<email-input
|
||||||
v-if="isTemplateEmail"
|
v-if="isTemplateEmail"
|
||||||
:message-id="messageId"
|
:message-id="messageId"
|
||||||
|
@ -60,6 +65,7 @@ import ChatOptions from 'shared/components/ChatOptions';
|
||||||
import ChatArticle from './template/Article';
|
import ChatArticle from './template/Article';
|
||||||
import EmailInput from './template/EmailInput';
|
import EmailInput from './template/EmailInput';
|
||||||
import CustomerSatisfaction from 'shared/components/CustomerSatisfaction';
|
import CustomerSatisfaction from 'shared/components/CustomerSatisfaction';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AgentMessageBubble',
|
name: 'AgentMessageBubble',
|
||||||
|
@ -71,7 +77,7 @@ export default {
|
||||||
EmailInput,
|
EmailInput,
|
||||||
CustomerSatisfaction,
|
CustomerSatisfaction,
|
||||||
},
|
},
|
||||||
mixins: [messageFormatterMixin],
|
mixins: [messageFormatterMixin, darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
message: { type: String, default: null },
|
message: { type: String, default: null },
|
||||||
contentType: { type: String, default: null },
|
contentType: { type: String, default: null },
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
<div class="agent-message">
|
<div class="agent-message">
|
||||||
<div class="avatar-wrap"></div>
|
<div class="avatar-wrap"></div>
|
||||||
<div class="message-wrap">
|
<div class="message-wrap">
|
||||||
<div class="typing-bubble chat-bubble agent">
|
<div
|
||||||
|
class="typing-bubble chat-bubble agent"
|
||||||
|
:class="$dm('bg-white', 'dark:bg-slate-50')"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src="~widget/assets/images/typing.gif"
|
src="~widget/assets/images/typing.gif"
|
||||||
alt="Agent is typing a message"
|
alt="Agent is typing a message"
|
||||||
|
@ -15,8 +18,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import darkModeMixing from 'widget/mixins/darkModeMixin.js';
|
||||||
export default {
|
export default {
|
||||||
name: 'AgentTypingBubble',
|
name: 'AgentTypingBubble',
|
||||||
|
mixins: [darkModeMixing],
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<header class="flex justify-between p-5 w-full">
|
<header
|
||||||
|
class="flex justify-between p-5 w-full"
|
||||||
|
:class="$dm('bg-white', 'dark:bg-slate-900')"
|
||||||
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<button v-if="showBackButton" @click="onBackButtonClick">
|
<button v-if="showBackButton" @click="onBackButtonClick">
|
||||||
<fluent-icon icon="chevron-left" size="24" />
|
<fluent-icon
|
||||||
|
icon="chevron-left"
|
||||||
|
size="24"
|
||||||
|
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<img
|
<img
|
||||||
v-if="avatarUrl"
|
v-if="avatarUrl"
|
||||||
|
@ -11,7 +18,10 @@
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-black-900 font-medium text-base flex items-center">
|
<div
|
||||||
|
class="font-medium text-base flex items-center"
|
||||||
|
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
||||||
|
>
|
||||||
<span class="mr-1" v-html="title" />
|
<span class="mr-1" v-html="title" />
|
||||||
<div
|
<div
|
||||||
:class="
|
:class="
|
||||||
|
@ -20,7 +30,10 @@
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs mt-1 text-black-700">
|
<div
|
||||||
|
class="text-xs mt-1"
|
||||||
|
:class="$dm('text-black-700', 'dark:text-slate-400')"
|
||||||
|
>
|
||||||
{{ replyWaitMessage }}
|
{{ replyWaitMessage }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,6 +49,7 @@ import availabilityMixin from 'widget/mixins/availability';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
import HeaderActions from './HeaderActions';
|
import HeaderActions from './HeaderActions';
|
||||||
import routerMixin from 'widget/mixins/routerMixin';
|
import routerMixin from 'widget/mixins/routerMixin';
|
||||||
|
import darkMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChatHeader',
|
name: 'ChatHeader',
|
||||||
|
@ -43,7 +57,7 @@ export default {
|
||||||
FluentIcon,
|
FluentIcon,
|
||||||
HeaderActions,
|
HeaderActions,
|
||||||
},
|
},
|
||||||
mixins: [availabilityMixin, routerMixin],
|
mixins: [availabilityMixin, routerMixin, darkMixin],
|
||||||
props: {
|
props: {
|
||||||
avatarUrl: {
|
avatarUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -67,7 +81,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({ widgetColor: 'appConfig/getWidgetColor' }),
|
...mapGetters({
|
||||||
|
widgetColor: 'appConfig/getWidgetColor',
|
||||||
|
}),
|
||||||
isOnline() {
|
isOnline() {
|
||||||
const { workingHoursEnabled } = this.channelConfig;
|
const { workingHoursEnabled } = this.channelConfig;
|
||||||
const anyAgentOnline = this.availableAgents.length > 0;
|
const anyAgentOnline = this.availableAgents.length > 0;
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<header class="header-expanded bg-white py-6 px-5 relative box-border w-full">
|
<header
|
||||||
|
class="header-expanded py-6 px-5 relative box-border w-full"
|
||||||
|
:class="$dm('bg-white', 'dark:bg-slate-900')"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-start"
|
class="flex items-start"
|
||||||
:class="[avatarUrl ? 'justify-between' : 'justify-end']"
|
:class="[avatarUrl ? 'justify-between' : 'justify-end']"
|
||||||
|
@ -8,21 +11,29 @@
|
||||||
<header-actions :show-popout-button="showPopoutButton" />
|
<header-actions :show-popout-button="showPopoutButton" />
|
||||||
</div>
|
</div>
|
||||||
<h2
|
<h2
|
||||||
class="text-slate-900 mt-5 text-3xl mb-3 font-normal"
|
class=" mt-5 text-3xl mb-3 font-normal"
|
||||||
|
:class="$dm('text-slate-900', 'dark:text-slate-50')"
|
||||||
v-html="introHeading"
|
v-html="introHeading"
|
||||||
/>
|
/>
|
||||||
<p class="text-lg text-black-700 leading-normal" v-html="introBody" />
|
<p
|
||||||
|
class="text-lg leading-normal"
|
||||||
|
:class="$dm('text-slate-700', 'dark:text-slate-200')"
|
||||||
|
v-html="introBody"
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import HeaderActions from './HeaderActions';
|
import HeaderActions from './HeaderActions';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChatHeaderExpanded',
|
name: 'ChatHeaderExpanded',
|
||||||
components: {
|
components: {
|
||||||
HeaderActions,
|
HeaderActions,
|
||||||
},
|
},
|
||||||
|
mixins: [darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
avatarUrl: {
|
avatarUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="chat-message--input"
|
class="chat-message--input is-focused"
|
||||||
:class="{ 'is-focused': isFocused }"
|
:class="$dm('bg-white ', 'dark:bg-slate-600')"
|
||||||
@keydown.esc="hideEmojiPicker"
|
@keydown.esc="hideEmojiPicker"
|
||||||
>
|
>
|
||||||
<resizable-text-area
|
<resizable-text-area
|
||||||
|
@ -10,7 +10,8 @@
|
||||||
v-model="userInput"
|
v-model="userInput"
|
||||||
:aria-label="$t('CHAT_PLACEHOLDER')"
|
:aria-label="$t('CHAT_PLACEHOLDER')"
|
||||||
:placeholder="$t('CHAT_PLACEHOLDER')"
|
:placeholder="$t('CHAT_PLACEHOLDER')"
|
||||||
class="form-input user-message-input"
|
class="form-input user-message-input is-focused"
|
||||||
|
:class="inputColor"
|
||||||
@typing-off="onTypingOff"
|
@typing-off="onTypingOff"
|
||||||
@typing-on="onTypingOn"
|
@typing-on="onTypingOn"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
|
@ -19,6 +20,7 @@
|
||||||
<div class="button-wrap">
|
<div class="button-wrap">
|
||||||
<chat-attachment-button
|
<chat-attachment-button
|
||||||
v-if="showAttachment"
|
v-if="showAttachment"
|
||||||
|
:class="$dm('text-black-900', 'dark:text-slate-100')"
|
||||||
:on-attach="onSendAttachment"
|
:on-attach="onSendAttachment"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
@ -27,10 +29,7 @@
|
||||||
aria-label="Emoji picker"
|
aria-label="Emoji picker"
|
||||||
@click="toggleEmojiPicker"
|
@click="toggleEmojiPicker"
|
||||||
>
|
>
|
||||||
<fluent-icon
|
<fluent-icon icon="emoji" :class="emojiIconColor" />
|
||||||
icon="emoji"
|
|
||||||
:class="{ 'text-woot-500': showEmojiPicker }"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
<emoji-input
|
<emoji-input
|
||||||
v-if="showEmojiPicker"
|
v-if="showEmojiPicker"
|
||||||
|
@ -57,6 +56,7 @@ import configMixin from '../mixins/configMixin';
|
||||||
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChatInputWrap',
|
name: 'ChatInputWrap',
|
||||||
|
@ -67,7 +67,7 @@ export default {
|
||||||
FluentIcon,
|
FluentIcon,
|
||||||
ResizableTextArea,
|
ResizableTextArea,
|
||||||
},
|
},
|
||||||
mixins: [clickaway, configMixin],
|
mixins: [clickaway, configMixin, darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
onSendMessage: {
|
onSendMessage: {
|
||||||
type: Function,
|
type: Function,
|
||||||
|
@ -98,6 +98,15 @@ export default {
|
||||||
showSendButton() {
|
showSendButton() {
|
||||||
return this.userInput.length > 0;
|
return this.userInput.length > 0;
|
||||||
},
|
},
|
||||||
|
inputColor() {
|
||||||
|
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
|
||||||
|
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
|
||||||
|
},
|
||||||
|
emojiIconColor() {
|
||||||
|
return this.showEmojiPicker
|
||||||
|
? `text-woot-500 ${this.$dm('text-black-900', 'dark:text-slate-100')}`
|
||||||
|
: `${this.$dm('text-black-900', 'dark:text-slate-100')}`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
isWidgetOpen(isWidgetOpen) {
|
isWidgetOpen(isWidgetOpen) {
|
||||||
|
|
|
@ -1,22 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<label class="block">
|
<label class="block">
|
||||||
<div
|
<div v-if="label" class="mb-2 text-xs font-medium" :class="labelClass">
|
||||||
v-if="label"
|
|
||||||
class="mb-2 text-xs font-medium"
|
|
||||||
:class="{
|
|
||||||
'text-black-800': !error,
|
|
||||||
'text-red-400': error,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
:type="type"
|
:type="type"
|
||||||
class="border rounded w-full py-2 px-3 text-slate-700 leading-tight outline-none"
|
class="border rounded w-full py-2 px-3 leading-tight outline-none"
|
||||||
:class="{
|
:class="inputHasError"
|
||||||
'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"
|
:placeholder="placeholder"
|
||||||
:value="value"
|
:value="value"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
|
@ -27,7 +17,9 @@
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -50,6 +42,27 @@ export default {
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
labelClass() {
|
||||||
|
return this.error
|
||||||
|
? `text-red-400 ${this.$dm('text-black-800', 'dark:text-slate-50')}`
|
||||||
|
: `text-black-800 ${this.$dm('text-black-800', 'dark:text-slate-50')}`;
|
||||||
|
},
|
||||||
|
isInputDarkOrLightMode() {
|
||||||
|
return `${this.$dm('bg-white', 'dark:bg-slate-600')} ${this.$dm(
|
||||||
|
'text-slate-700',
|
||||||
|
'dark:text-slate-50'
|
||||||
|
)}`;
|
||||||
|
},
|
||||||
|
inputBorderColor() {
|
||||||
|
return `${this.$dm('border-black-200', 'dark:border-black-500')}`;
|
||||||
|
},
|
||||||
|
inputHasError() {
|
||||||
|
return this.error
|
||||||
|
? `border-red-200 hover:border-red-300 focus:border-red-300 ${this.isInputDarkOrLightMode}`
|
||||||
|
: `hover:border-black-300 focus:border-black-300 ${this.isInputDarkOrLightMode} ${this.inputBorderColor}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onChange(event) {
|
onChange(event) {
|
||||||
this.$emit('input', event.target.value);
|
this.$emit('input', event.target.value);
|
||||||
|
|
|
@ -1,21 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<label class="block">
|
<label class="block">
|
||||||
<div
|
<div v-if="label" class="mb-2 text-xs font-medium" :class="labelClass">
|
||||||
v-if="label"
|
|
||||||
class="mb-2 text-xs font-medium"
|
|
||||||
:class="{
|
|
||||||
'text-black-800': !error,
|
|
||||||
'text-red-400': error,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
class="resize-none border rounded w-full py-2 px-3 text-slate-700 leading-tight outline-none"
|
class="resize-none border rounded w-full py-2 px-3 text-slate-700 leading-tight outline-none"
|
||||||
:class="{
|
:class="isTextAreaHasError"
|
||||||
'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"
|
:placeholder="placeholder"
|
||||||
:value="value"
|
:value="value"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
|
@ -26,7 +16,9 @@
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -49,6 +41,27 @@ export default {
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
labelClass() {
|
||||||
|
return this.error
|
||||||
|
? `text-red-400 ${this.$dm('text-black-800', 'dark:text-slate-50')}`
|
||||||
|
: `text-black-800 ${this.$dm('text-black-800', 'dark:text-slate-50')}`;
|
||||||
|
},
|
||||||
|
isTextAreaDarkOrLightMode() {
|
||||||
|
return `${this.$dm('bg-white', 'dark:bg-slate-600')} ${this.$dm(
|
||||||
|
'text-slate-700',
|
||||||
|
'dark:text-slate-50'
|
||||||
|
)}`;
|
||||||
|
},
|
||||||
|
textAreaBorderColor() {
|
||||||
|
return `${this.$dm('border-black-200', 'dark:border-black-500')}`;
|
||||||
|
},
|
||||||
|
isTextAreaHasError() {
|
||||||
|
return this.error
|
||||||
|
? `border-red-200 hover:border-red-300 focus:border-red-300 ${this.isTextAreaDarkOrLightMode}`
|
||||||
|
: `hover:border-black-300 focus:border-black-300 ${this.isTextAreaDarkOrLightMode} ${this.textAreaBorderColor}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onChange(event) {
|
onChange(event) {
|
||||||
this.$emit('input', event.target.value);
|
this.$emit('input', event.target.value);
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="showHeaderActions" class="actions flex items-center">
|
<div v-if="showHeaderActions" class="actions flex items-center">
|
||||||
<button
|
<button
|
||||||
v-if="conversationStatus === 'open'"
|
v-if="conversationStatus === 'open' && hasEndConversationEnabled"
|
||||||
class="button transparent compact"
|
class="button transparent compact"
|
||||||
:title="$t('END_CONVERSATION')"
|
:title="$t('END_CONVERSATION')"
|
||||||
@click="resolveConversation"
|
@click="resolveConversation"
|
||||||
>
|
>
|
||||||
<fluent-icon icon="sign-out" size="22" class="text-black-900" />
|
<fluent-icon
|
||||||
|
icon="sign-out"
|
||||||
|
size="22"
|
||||||
|
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="showPopoutButton"
|
v-if="showPopoutButton"
|
||||||
class="button transparent compact new-window--button "
|
class="button transparent compact new-window--button "
|
||||||
@click="popoutWindow"
|
@click="popoutWindow"
|
||||||
>
|
>
|
||||||
<fluent-icon icon="open" size="22" class="text-black-900" />
|
<fluent-icon
|
||||||
|
icon="open"
|
||||||
|
size="22"
|
||||||
|
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button transparent compact close-button"
|
class="button transparent compact close-button"
|
||||||
|
@ -22,7 +30,11 @@
|
||||||
}"
|
}"
|
||||||
@click="closeWindow"
|
@click="closeWindow"
|
||||||
>
|
>
|
||||||
<fluent-icon icon="dismiss" size="24" class="text-black-900" />
|
<fluent-icon
|
||||||
|
icon="dismiss"
|
||||||
|
size="24"
|
||||||
|
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -31,10 +43,13 @@ import { mapGetters } from 'vuex';
|
||||||
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
|
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
|
||||||
import { popoutChatWindow } from '../helpers/popoutHelper';
|
import { popoutChatWindow } from '../helpers/popoutHelper';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||||
|
import configMixin from 'widget/mixins/configMixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HeaderActions',
|
name: 'HeaderActions',
|
||||||
components: { FluentIcon },
|
components: { FluentIcon },
|
||||||
|
mixins: [configMixin, darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
showPopoutButton: {
|
showPopoutButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="shouldShowHeaderMessage"
|
v-if="shouldShowHeaderMessage"
|
||||||
class="text-black-800 text-sm leading-5"
|
class="text-sm leading-5"
|
||||||
|
:class="$dm('text-black-800', 'dark:text-slate-50')"
|
||||||
>
|
>
|
||||||
{{ headerMessage }}
|
{{ headerMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -64,6 +65,7 @@ import { getContrastingTextColor } from '@chatwoot/utils';
|
||||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||||
import { isEmptyObject } from 'widget/helpers/utils';
|
import { isEmptyObject } from 'widget/helpers/utils';
|
||||||
import routerMixin from 'widget/mixins/routerMixin';
|
import routerMixin from 'widget/mixins/routerMixin';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FormInput,
|
FormInput,
|
||||||
|
@ -71,7 +73,7 @@ export default {
|
||||||
CustomButton,
|
CustomButton,
|
||||||
Spinner,
|
Spinner,
|
||||||
},
|
},
|
||||||
mixins: [routerMixin],
|
mixins: [routerMixin, darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="px-5">
|
<div class="px-5">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="text-black-700 max-w-xs">
|
<div
|
||||||
|
class="max-w-xs"
|
||||||
|
:class="$dm('text-black-700', 'dark:text-slate-50')"
|
||||||
|
>
|
||||||
<div class="text-base leading-5 font-medium mb-1">
|
<div class="text-base leading-5 font-medium mb-1">
|
||||||
{{
|
{{
|
||||||
isOnline
|
isOnline
|
||||||
|
@ -36,6 +39,7 @@ import AvailableAgents from 'widget/components/AvailableAgents.vue';
|
||||||
import CustomButton from 'shared/components/Button';
|
import CustomButton from 'shared/components/Button';
|
||||||
import configMixin from 'widget/mixins/configMixin';
|
import configMixin from 'widget/mixins/configMixin';
|
||||||
import availabilityMixin from 'widget/mixins/availability';
|
import availabilityMixin from 'widget/mixins/availability';
|
||||||
|
import darkMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TeamAvailability',
|
name: 'TeamAvailability',
|
||||||
|
@ -43,7 +47,7 @@ export default {
|
||||||
AvailableAgents,
|
AvailableAgents,
|
||||||
CustomButton,
|
CustomButton,
|
||||||
},
|
},
|
||||||
mixins: [configMixin, availabilityMixin],
|
mixins: [configMixin, availabilityMixin, darkMixin],
|
||||||
props: {
|
props: {
|
||||||
availableAgents: {
|
availableAgents: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -55,7 +59,9 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({ widgetColor: 'appConfig/getWidgetColor' }),
|
...mapGetters({
|
||||||
|
widgetColor: 'appConfig/getWidgetColor',
|
||||||
|
}),
|
||||||
textColor() {
|
textColor() {
|
||||||
return getContrastingTextColor(this.widgetColor);
|
return getContrastingTextColor(this.widgetColor);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="chat-bubble-wrap">
|
<div class="chat-bubble-wrap">
|
||||||
<button class="chat-bubble agent" @click="onClickMessage">
|
<button
|
||||||
|
class="chat-bubble agent"
|
||||||
|
:class="$dm('bg-white', 'dark:bg-slate-50')"
|
||||||
|
@click="onClickMessage"
|
||||||
|
>
|
||||||
<div v-if="showSender" class="row--agent-block">
|
<div v-if="showSender" class="row--agent-block">
|
||||||
<thumbnail
|
<thumbnail
|
||||||
:src="avatarUrl"
|
:src="avatarUrl"
|
||||||
|
@ -25,10 +29,11 @@ import {
|
||||||
ON_CAMPAIGN_MESSAGE_CLICK,
|
ON_CAMPAIGN_MESSAGE_CLICK,
|
||||||
ON_UNREAD_MESSAGE_CLICK,
|
ON_UNREAD_MESSAGE_CLICK,
|
||||||
} from '../constants/widgetBusEvents';
|
} from '../constants/widgetBusEvents';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||||
export default {
|
export default {
|
||||||
name: 'UnreadMessage',
|
name: 'UnreadMessage',
|
||||||
components: { Thumbnail },
|
components: { Thumbnail },
|
||||||
mixins: [messageFormatterMixin, configMixin],
|
mixins: [messageFormatterMixin, configMixin, darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
message: {
|
message: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="w-full h-full bg-slate-50 flex flex-col"
|
class="w-full h-full flex flex-col"
|
||||||
|
:class="$dm('bg-slate-50', 'dark:bg-slate-800')"
|
||||||
@keydown.esc="closeWindow"
|
@keydown.esc="closeWindow"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="header-wrap bg-white"
|
class="header-wrap"
|
||||||
:class="{ expanded: !isHeaderCollapsed, collapsed: isHeaderCollapsed }"
|
:class="{
|
||||||
|
expanded: !isHeaderCollapsed,
|
||||||
|
collapsed: isHeaderCollapsed,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="transition-all delay-200 duration-300 ease-in"
|
enter-active-class="transition-all delay-200 duration-300 ease-in"
|
||||||
|
@ -51,6 +55,7 @@ import Branding from 'shared/components/Branding.vue';
|
||||||
import ChatHeader from '../ChatHeader.vue';
|
import ChatHeader from '../ChatHeader.vue';
|
||||||
import ChatHeaderExpanded from '../ChatHeaderExpanded.vue';
|
import ChatHeaderExpanded from '../ChatHeaderExpanded.vue';
|
||||||
import configMixin from '../../mixins/configMixin';
|
import configMixin from '../../mixins/configMixin';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { IFrameHelper } from 'widget/helpers/utils';
|
import { IFrameHelper } from 'widget/helpers/utils';
|
||||||
|
|
||||||
|
@ -61,7 +66,7 @@ export default {
|
||||||
ChatHeader,
|
ChatHeader,
|
||||||
ChatHeaderExpanded,
|
ChatHeaderExpanded,
|
||||||
},
|
},
|
||||||
mixins: [configMixin],
|
mixins: [configMixin, darkModeMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showPopoutButton: false,
|
showPopoutButton: false,
|
||||||
|
|
|
@ -1,12 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!!items.length" class="chat-bubble agent">
|
<div
|
||||||
|
v-if="!!items.length"
|
||||||
|
class="chat-bubble agent"
|
||||||
|
:class="$dm('bg-white', 'dark:bg-slate-700')"
|
||||||
|
>
|
||||||
<div v-for="item in items" :key="item.link" class="article-item">
|
<div v-for="item in items" :key="item.link" class="article-item">
|
||||||
<a :href="item.link" target="_blank" rel="noopener noreferrer nofollow">
|
<a :href="item.link" target="_blank" rel="noopener noreferrer nofollow">
|
||||||
<span class="title flex items-center text-black-900 font-medium">
|
<span class="title flex items-center text-black-900 font-medium">
|
||||||
<fluent-icon icon="link" class="mr-1" />
|
<fluent-icon
|
||||||
<span>{{ item.title }}</span>
|
icon="link"
|
||||||
|
class="mr-1"
|
||||||
|
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
||||||
|
/>
|
||||||
|
<span :class="$dm('text-slate-900', 'dark:text-slate-50')">{{
|
||||||
|
item.title
|
||||||
|
}}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="description">
|
<span
|
||||||
|
class="description"
|
||||||
|
:class="$dm('text-slate-700', 'dark:text-slate-200')"
|
||||||
|
>
|
||||||
{{ truncateMessage(item.description) }}
|
{{ truncateMessage(item.description) }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -17,12 +30,13 @@
|
||||||
<script>
|
<script>
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FluentIcon,
|
FluentIcon,
|
||||||
},
|
},
|
||||||
mixins: [messageFormatterMixin],
|
mixins: [messageFormatterMixin, darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
v-model.trim="email"
|
v-model.trim="email"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
:placeholder="$t('EMAIL_PLACEHOLDER')"
|
:placeholder="$t('EMAIL_PLACEHOLDER')"
|
||||||
:class="{ error: $v.email.$error }"
|
:class="inputHasError"
|
||||||
@input="$v.email.$touch"
|
@input="$v.email.$touch"
|
||||||
@keydown.enter="onSubmit"
|
@keydown.enter="onSubmit"
|
||||||
/>
|
/>
|
||||||
|
@ -31,12 +31,14 @@ import { required, email } from 'vuelidate/lib/validators';
|
||||||
|
|
||||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
import Spinner from 'shared/components/Spinner';
|
import Spinner from 'shared/components/Spinner';
|
||||||
|
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FluentIcon,
|
FluentIcon,
|
||||||
Spinner,
|
Spinner,
|
||||||
},
|
},
|
||||||
|
mixins: [darkModeMixin],
|
||||||
props: {
|
props: {
|
||||||
messageId: {
|
messageId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
@ -63,6 +65,15 @@ export default {
|
||||||
this.messageContentAttributes.submitted_email
|
this.messageContentAttributes.submitted_email
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
inputColor() {
|
||||||
|
return `${this.$dm('bg-white', 'dark:bg-slate-600')}
|
||||||
|
${this.$dm('text-black-900', 'dark:text-slate-50')}`;
|
||||||
|
},
|
||||||
|
inputHasError() {
|
||||||
|
return this.$v.email.$error
|
||||||
|
? `${this.inputColor} error`
|
||||||
|
: `${this.inputColor}`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
validations: {
|
validations: {
|
||||||
email: {
|
email: {
|
||||||
|
@ -105,6 +116,10 @@ export default {
|
||||||
padding: $space-one;
|
padding: $space-one;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $color-light-gray;
|
||||||
|
}
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
border-color: $color-error;
|
border-color: $color-error;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,9 @@ export default {
|
||||||
hasAttachmentsEnabled() {
|
hasAttachmentsEnabled() {
|
||||||
return this.channelConfig.enabledFeatures.includes('attachments');
|
return this.channelConfig.enabledFeatures.includes('attachments');
|
||||||
},
|
},
|
||||||
|
hasEndConversationEnabled() {
|
||||||
|
return this.channelConfig.enabledFeatures.includes('end_conversation');
|
||||||
|
},
|
||||||
preChatFormEnabled() {
|
preChatFormEnabled() {
|
||||||
return window.chatwootWebChannel.preChatFormEnabled;
|
return window.chatwootWebChannel.preChatFormEnabled;
|
||||||
},
|
},
|
||||||
|
|
15
app/javascript/widget/mixins/darkModeMixin.js
Normal file
15
app/javascript/widget/mixins/darkModeMixin.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ darkMode: 'appConfig/darkMode' }),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
$dm(light, dark) {
|
||||||
|
if (this.darkMode === 'light') {
|
||||||
|
return light;
|
||||||
|
}
|
||||||
|
return light + ' ' + dark;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -5,7 +5,7 @@ import Vue from 'vue';
|
||||||
global.chatwootWebChannel = {
|
global.chatwootWebChannel = {
|
||||||
avatarUrl: 'https://test.url',
|
avatarUrl: 'https://test.url',
|
||||||
hasAConnectedAgentBot: 'AgentBot',
|
hasAConnectedAgentBot: 'AgentBot',
|
||||||
enabledFeatures: ['emoji_picker', 'attachments'],
|
enabledFeatures: ['emoji_picker', 'attachments', 'end_conversation'],
|
||||||
preChatFormOptions: { require_email: false, pre_chat_message: '' },
|
preChatFormOptions: { require_email: false, pre_chat_message: '' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ describe('configMixin', () => {
|
||||||
const vm = new Constructor().$mount();
|
const vm = new Constructor().$mount();
|
||||||
const wrapper = createWrapper(vm);
|
const wrapper = createWrapper(vm);
|
||||||
expect(wrapper.vm.hasEmojiPickerEnabled).toBe(true);
|
expect(wrapper.vm.hasEmojiPickerEnabled).toBe(true);
|
||||||
|
expect(wrapper.vm.hasEndConversationEnabled).toBe(true);
|
||||||
expect(wrapper.vm.hasAttachmentsEnabled).toBe(true);
|
expect(wrapper.vm.hasAttachmentsEnabled).toBe(true);
|
||||||
expect(wrapper.vm.hasAConnectedAgentBot).toBe(true);
|
expect(wrapper.vm.hasAConnectedAgentBot).toBe(true);
|
||||||
expect(wrapper.vm.useInboxAvatarForBot).toBe(true);
|
expect(wrapper.vm.useInboxAvatarForBot).toBe(true);
|
||||||
|
@ -31,7 +32,7 @@ describe('configMixin', () => {
|
||||||
expect(wrapper.vm.channelConfig).toEqual({
|
expect(wrapper.vm.channelConfig).toEqual({
|
||||||
avatarUrl: 'https://test.url',
|
avatarUrl: 'https://test.url',
|
||||||
hasAConnectedAgentBot: 'AgentBot',
|
hasAConnectedAgentBot: 'AgentBot',
|
||||||
enabledFeatures: ['emoji_picker', 'attachments'],
|
enabledFeatures: ['emoji_picker', 'attachments', 'end_conversation'],
|
||||||
preChatFormOptions: {
|
preChatFormOptions: {
|
||||||
pre_chat_message: '',
|
pre_chat_message: '',
|
||||||
require_email: false,
|
require_email: false,
|
||||||
|
|
41
app/javascript/widget/mixins/specs/darkModeMixin.spec.js
Normal file
41
app/javascript/widget/mixins/specs/darkModeMixin.spec.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
|
import darkModeMixin from '../darkModeMixin';
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
const localVue = createLocalVue();
|
||||||
|
localVue.use(Vuex);
|
||||||
|
|
||||||
|
const darkModeValues = ['light', 'auto'];
|
||||||
|
|
||||||
|
describe('darkModeMixin', () => {
|
||||||
|
let getters;
|
||||||
|
let store;
|
||||||
|
beforeEach(() => {
|
||||||
|
getters = {
|
||||||
|
'appConfig/darkMode': () => darkModeValues[0],
|
||||||
|
};
|
||||||
|
store = new Vuex.Store({ getters });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if light theme', () => {
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
mixins: [darkModeMixin],
|
||||||
|
};
|
||||||
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
|
expect(wrapper.vm.$dm('bg-100', 'bg-600')).toBe('bg-100');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if auto theme', () => {
|
||||||
|
getters = {
|
||||||
|
'appConfig/darkMode': () => darkModeValues[2],
|
||||||
|
};
|
||||||
|
store = new Vuex.Store({ getters });
|
||||||
|
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
mixins: [darkModeMixin],
|
||||||
|
};
|
||||||
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
|
expect(wrapper.vm.$dm('bg-100', 'bg-600')).toBe('bg-100 bg-600');
|
||||||
|
});
|
||||||
|
});
|
|
@ -15,6 +15,7 @@ const state = {
|
||||||
showPopoutButton: false,
|
showPopoutButton: false,
|
||||||
widgetColor: '',
|
widgetColor: '',
|
||||||
widgetStyle: 'standard',
|
widgetStyle: 'standard',
|
||||||
|
darkMode: 'light',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
@ -25,18 +26,26 @@ export const getters = {
|
||||||
getWidgetColor: $state => $state.widgetColor,
|
getWidgetColor: $state => $state.widgetColor,
|
||||||
getReferrerHost: $state => $state.referrerHost,
|
getReferrerHost: $state => $state.referrerHost,
|
||||||
isWidgetStyleFlat: $state => $state.widgetStyle === 'flat',
|
isWidgetStyleFlat: $state => $state.widgetStyle === 'flat',
|
||||||
|
darkMode: $state => $state.darkMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
setAppConfig(
|
setAppConfig(
|
||||||
{ commit },
|
{ commit },
|
||||||
{ showPopoutButton, position, hideMessageBubble, widgetStyle = 'rounded' }
|
{
|
||||||
|
showPopoutButton,
|
||||||
|
position,
|
||||||
|
hideMessageBubble,
|
||||||
|
widgetStyle = 'rounded',
|
||||||
|
darkMode = 'light',
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
commit(SET_WIDGET_APP_CONFIG, {
|
commit(SET_WIDGET_APP_CONFIG, {
|
||||||
hideMessageBubble: !!hideMessageBubble,
|
hideMessageBubble: !!hideMessageBubble,
|
||||||
position: position || 'right',
|
position: position || 'right',
|
||||||
showPopoutButton: !!showPopoutButton,
|
showPopoutButton: !!showPopoutButton,
|
||||||
widgetStyle,
|
widgetStyle,
|
||||||
|
darkMode,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
toggleWidgetOpen({ commit }, isWidgetOpen) {
|
toggleWidgetOpen({ commit }, isWidgetOpen) {
|
||||||
|
@ -56,6 +65,7 @@ export const mutations = {
|
||||||
$state.position = data.position;
|
$state.position = data.position;
|
||||||
$state.hideMessageBubble = data.hideMessageBubble;
|
$state.hideMessageBubble = data.hideMessageBubble;
|
||||||
$state.widgetStyle = data.widgetStyle;
|
$state.widgetStyle = data.widgetStyle;
|
||||||
|
$state.darkMode = data.darkMode;
|
||||||
},
|
},
|
||||||
[TOGGLE_WIDGET_OPEN]($state, isWidgetOpen) {
|
[TOGGLE_WIDGET_OPEN]($state, isWidgetOpen) {
|
||||||
$state.isWidgetOpen = isWidgetOpen;
|
$state.isWidgetOpen = isWidgetOpen;
|
||||||
|
|
|
@ -84,8 +84,12 @@ export const actions = {
|
||||||
fetchOldConversations: async ({ commit }, { before } = {}) => {
|
fetchOldConversations: async ({ commit }, { before } = {}) => {
|
||||||
try {
|
try {
|
||||||
commit('setConversationListLoading', true);
|
commit('setConversationListLoading', true);
|
||||||
const { data } = await getMessagesAPI({ before });
|
const {
|
||||||
const formattedMessages = getNonDeletedMessages({ messages: data });
|
data: { payload, meta },
|
||||||
|
} = await getMessagesAPI({ before });
|
||||||
|
const { contact_last_seen_at: lastSeen } = meta;
|
||||||
|
const formattedMessages = getNonDeletedMessages({ messages: payload });
|
||||||
|
commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true });
|
||||||
commit('setMessagesInConversation', formattedMessages);
|
commit('setMessagesInConversation', formattedMessages);
|
||||||
commit('setConversationListLoading', false);
|
commit('setConversationListLoading', false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -181,7 +181,8 @@ describe('#actions', () => {
|
||||||
describe('#fetchOldConversations', () => {
|
describe('#fetchOldConversations', () => {
|
||||||
it('sends correct actions', async () => {
|
it('sends correct actions', async () => {
|
||||||
API.get.mockResolvedValue({
|
API.get.mockResolvedValue({
|
||||||
data: [
|
data: {
|
||||||
|
payload: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
text: 'hey',
|
text: 'hey',
|
||||||
|
@ -193,10 +194,15 @@ describe('#actions', () => {
|
||||||
content_attributes: { deleted: true },
|
content_attributes: { deleted: true },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
meta: {
|
||||||
|
contact_last_seen_at: 1466424490,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await actions.fetchOldConversations({ commit }, {});
|
await actions.fetchOldConversations({ commit }, {});
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
['setConversationListLoading', true],
|
['setConversationListLoading', true],
|
||||||
|
['conversation/setMetaUserLastSeenAt', 1466424490, { root: true }],
|
||||||
[
|
[
|
||||||
'setMessagesInConversation',
|
'setMessagesInConversation',
|
||||||
[
|
[
|
||||||
|
|
|
@ -8,8 +8,9 @@ class BaseListener
|
||||||
|
|
||||||
def extract_notification_and_account(event)
|
def extract_notification_and_account(event)
|
||||||
notification = event.data[:notification]
|
notification = event.data[:notification]
|
||||||
unread_count = notification.user.notifications_meta[:unread_count]
|
notifications_meta = notification.user.notifications_meta(notification.account_id)
|
||||||
count = notification.user.notifications_meta[:count]
|
unread_count = notifications_meta[:unread_count]
|
||||||
|
count = notifications_meta[:count]
|
||||||
[notification, notification.account, unread_count, count]
|
[notification, notification.account, unread_count, count]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ class AutomationRule < ApplicationRecord
|
||||||
|
|
||||||
scope :active, -> { where(active: true) }
|
scope :active, -> { where(active: true) }
|
||||||
|
|
||||||
CONDITIONS_ATTRS = %w[email country_code status message_type browser_language assignee_id team_id referer city company].freeze
|
CONDITIONS_ATTRS = %w[content email country_code status message_type browser_language assignee_id team_id referrer city company].freeze
|
||||||
ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agents send_attachments].freeze
|
ACTIONS_ATTRS = %w[send_message add_label send_email_to_team assign_team assign_best_agents send_attachments].freeze
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :integer not null, primary key
|
||||||
# continuity_via_email :boolean default(TRUE), not null
|
# continuity_via_email :boolean default(TRUE), not null
|
||||||
# feature_flags :integer default(3), not null
|
# feature_flags :integer default(7), not null
|
||||||
# hmac_mandatory :boolean default(FALSE)
|
# hmac_mandatory :boolean default(FALSE)
|
||||||
# hmac_token :string
|
# hmac_token :string
|
||||||
# pre_chat_form_enabled :boolean default(FALSE)
|
# pre_chat_form_enabled :boolean default(FALSE)
|
||||||
|
@ -43,6 +43,7 @@ class Channel::WebWidget < ApplicationRecord
|
||||||
|
|
||||||
has_flags 1 => :attachments,
|
has_flags 1 => :attachments,
|
||||||
2 => :emoji_picker,
|
2 => :emoji_picker,
|
||||||
|
3 => :end_conversation,
|
||||||
:column => 'feature_flags',
|
:column => 'feature_flags',
|
||||||
:check_for_column => false
|
:check_for_column => false
|
||||||
|
|
||||||
|
|
|
@ -188,10 +188,10 @@ class User < ApplicationRecord
|
||||||
mutations_from_database.changed?('email')
|
mutations_from_database.changed?('email')
|
||||||
end
|
end
|
||||||
|
|
||||||
def notifications_meta
|
def notifications_meta(account_id)
|
||||||
{
|
{
|
||||||
unread_count: notifications.where(read_at: nil).count,
|
unread_count: notifications.where(account_id: account_id, read_at: nil).count,
|
||||||
count: notifications.count
|
count: notifications.where(account_id: account_id).count
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,7 +33,7 @@ class MailPresenter < SimpleDelegator
|
||||||
# encodes mail raw body if mail.content_type is plain/text
|
# encodes mail raw body if mail.content_type is plain/text
|
||||||
# encodes mail raw body if mail.content_type is html/text
|
# encodes mail raw body if mail.content_type is html/text
|
||||||
def text_html_mail(mail_part)
|
def text_html_mail(mail_part)
|
||||||
decoded = mail_part&.decoded || @mail.body&.decoded
|
decoded = mail_part&.decoded || @mail.decoded
|
||||||
encoded = encode_to_unicode(decoded)
|
encoded = encode_to_unicode(decoded)
|
||||||
|
|
||||||
encoded if html_mail_body? || text_mail_body?
|
encoded if html_mail_body? || text_mail_body?
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
if @team_member
|
json.array! @team_members do |team_member|
|
||||||
json.partial! 'api/v1/models/agent.json.jbuilder', resource: @team_member
|
|
||||||
elsif @team_members.present?
|
|
||||||
json.array! @team_members do |team_member|
|
|
||||||
json.partial! 'api/v1/models/agent.json.jbuilder', resource: team_member
|
json.partial! 'api/v1/models/agent.json.jbuilder', resource: team_member
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
json.array! @messages do |message|
|
json.payload do
|
||||||
|
json.array! @messages do |message|
|
||||||
json.id message.id
|
json.id message.id
|
||||||
json.content message.content
|
json.content message.content
|
||||||
json.message_type message.message_type_before_type_cast
|
json.message_type message.message_type_before_type_cast
|
||||||
|
@ -8,4 +9,8 @@ json.array! @messages do |message|
|
||||||
json.conversation_id message.conversation.display_id
|
json.conversation_id message.conversation.display_id
|
||||||
json.attachments message.attachments.map(&:push_event_data) if message.attachments.present?
|
json.attachments message.attachments.map(&:push_event_data) if message.attachments.present?
|
||||||
json.sender message.sender.push_event_data if message.sender
|
json.sender message.sender.push_event_data if message.sender
|
||||||
|
end
|
||||||
|
end
|
||||||
|
json.meta do
|
||||||
|
json.contact_last_seen_at @conversation.contact_last_seen_at.to_i if @conversation.present?
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@ json.id resource.display_id
|
||||||
json.inbox_id resource.inbox_id
|
json.inbox_id resource.inbox_id
|
||||||
json.contact_last_seen_at resource.contact_last_seen_at.to_i
|
json.contact_last_seen_at resource.contact_last_seen_at.to_i
|
||||||
json.status resource.status
|
json.status resource.status
|
||||||
|
json.agent_last_seen_at resource.agent_last_seen_at.to_i
|
||||||
json.messages do
|
json.messages do
|
||||||
json.array! resource.messages do |message|
|
json.array! resource.messages do |message|
|
||||||
json.partial! 'public/api/v1/models/message.json.jbuilder', resource: message
|
json.partial! 'public/api/v1/models/message.json.jbuilder', resource: message
|
||||||
|
|
|
@ -19,6 +19,7 @@ window.chatwootSettings = {
|
||||||
type: '<%= @widget_type %>',
|
type: '<%= @widget_type %>',
|
||||||
showPopoutButton: true,
|
showPopoutButton: true,
|
||||||
widgetStyle: '<%= @widget_style %>',
|
widgetStyle: '<%= @widget_style %>',
|
||||||
|
darkMode: '<%= @dark_mode %>',
|
||||||
};
|
};
|
||||||
|
|
||||||
(function(d,t) {
|
(function(d,t) {
|
||||||
|
|
16
db/migrate/20220405092033_update_web_widget_feature_flags.rb
Normal file
16
db/migrate/20220405092033_update_web_widget_feature_flags.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
class UpdateWebWidgetFeatureFlags < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
change_column_default(:channel_web_widgets, :feature_flags, from: 3, to: 7)
|
||||||
|
|
||||||
|
set_end_conversation_to_default
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_end_conversation_to_default
|
||||||
|
::Channel::WebWidget.find_in_batches do |widget_batch|
|
||||||
|
widget_batch.each do |widget|
|
||||||
|
widget.end_conversation = true
|
||||||
|
widget.save!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2022_02_18_120357) do
|
ActiveRecord::Schema.define(version: 2022_04_05_092033) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
|
@ -280,7 +280,7 @@ ActiveRecord::Schema.define(version: 2022_02_18_120357) do
|
||||||
t.string "widget_color", default: "#1f93ff"
|
t.string "widget_color", default: "#1f93ff"
|
||||||
t.string "welcome_title"
|
t.string "welcome_title"
|
||||||
t.string "welcome_tagline"
|
t.string "welcome_tagline"
|
||||||
t.integer "feature_flags", default: 3, null: false
|
t.integer "feature_flags", default: 7, null: false
|
||||||
t.integer "reply_time", default: 0
|
t.integer "reply_time", default: 0
|
||||||
t.string "hmac_token"
|
t.string "hmac_token"
|
||||||
t.boolean "pre_chat_form_enabled", default: false
|
t.boolean "pre_chat_form_enabled", default: false
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# Description: Chatwoot installation script
|
# Description: Chatwoot installation script
|
||||||
# OS: Ubuntu 20.04 LTS / Ubuntu 20.10
|
# OS: Ubuntu 20.04 LTS / Ubuntu 20.10
|
||||||
# Script Version: 0.7
|
# Script Version: 0.8
|
||||||
# Run this script as root
|
# Run this script as root
|
||||||
|
|
||||||
read -p 'Would you like to configure a domain and SSL for Chatwoot?(yes or no): ' configure_webserver
|
read -p 'Would you like to configure a domain and SSL for Chatwoot?(yes or no): ' configure_webserver
|
||||||
|
@ -12,6 +12,7 @@ then
|
||||||
read -p 'Enter your sub-domain to be used for Chatwoot (chatwoot.domain.com for example) : ' domain_name
|
read -p 'Enter your sub-domain to be used for Chatwoot (chatwoot.domain.com for example) : ' domain_name
|
||||||
echo -e "\nThis script will try to generate SSL certificates via LetsEncrypt and serve chatwoot at
|
echo -e "\nThis script will try to generate SSL certificates via LetsEncrypt and serve chatwoot at
|
||||||
"https://$domain_name". Proceed further once you have pointed your DNS to the IP of the instance.\n"
|
"https://$domain_name". Proceed further once you have pointed your DNS to the IP of the instance.\n"
|
||||||
|
read -p 'Enter the email LetsEncrypt can use to send reminders when your SSL certificate is up for renewal: ' le_email
|
||||||
read -p 'Do you wish to proceed? (yes or no): ' exit_true
|
read -p 'Do you wish to proceed? (yes or no): ' exit_true
|
||||||
if [ $exit_true == "no" ]
|
if [ $exit_true == "no" ]
|
||||||
then
|
then
|
||||||
|
@ -138,7 +139,7 @@ else
|
||||||
curl https://ssl-config.mozilla.org/ffdhe4096.txt >> /etc/ssl/dhparam
|
curl https://ssl-config.mozilla.org/ffdhe4096.txt >> /etc/ssl/dhparam
|
||||||
wget https://raw.githubusercontent.com/chatwoot/chatwoot/develop/deployment/nginx_chatwoot.conf
|
wget https://raw.githubusercontent.com/chatwoot/chatwoot/develop/deployment/nginx_chatwoot.conf
|
||||||
cp nginx_chatwoot.conf /etc/nginx/sites-available/nginx_chatwoot.conf
|
cp nginx_chatwoot.conf /etc/nginx/sites-available/nginx_chatwoot.conf
|
||||||
certbot certonly --nginx -d $domain_name
|
certbot certonly --non-interactive --agree-tos --nginx -m $le_email -d $domain_name
|
||||||
sed -i "s/chatwoot.domain.com/$domain_name/g" /etc/nginx/sites-available/nginx_chatwoot.conf
|
sed -i "s/chatwoot.domain.com/$domain_name/g" /etc/nginx/sites-available/nginx_chatwoot.conf
|
||||||
ln -s /etc/nginx/sites-available/nginx_chatwoot.conf /etc/nginx/sites-enabled/nginx_chatwoot.conf
|
ln -s /etc/nginx/sites-available/nginx_chatwoot.conf /etc/nginx/sites-enabled/nginx_chatwoot.conf
|
||||||
systemctl restart nginx
|
systemctl restart nginx
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
module OnlineStatusTracker
|
module OnlineStatusTracker
|
||||||
PRESENCE_DURATION = 20.seconds
|
# NOTE: You can customise the environment variable to keep your agents/contacts as online for longer
|
||||||
|
PRESENCE_DURATION = ENV.fetch('PRESENCE_DURATION', 20).to_i.seconds
|
||||||
|
|
||||||
# presence : sorted set with timestamp as the score & object id as value
|
# presence : sorted set with timestamp as the score & object id as value
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,13 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
||||||
let(:web_widget) { create(:channel_widget, account: account) }
|
let(:web_widget) { create(:channel_widget, account: account) }
|
||||||
let(:contact) { create(:contact, account: account, email: nil) }
|
let(:contact) { create(:contact, account: account, email: nil) }
|
||||||
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
|
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
|
||||||
|
let(:second_session) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
|
||||||
let!(:conversation) { create(:conversation, contact: contact, account: account, inbox: web_widget.inbox, contact_inbox: contact_inbox) }
|
let!(:conversation) { create(:conversation, contact: contact, account: account, inbox: web_widget.inbox, contact_inbox: contact_inbox) }
|
||||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||||
let(:token) { ::Widget::TokenService.new(payload: payload).generate_token }
|
let(:token) { ::Widget::TokenService.new(payload: payload).generate_token }
|
||||||
|
let(:token_without_conversation) do
|
||||||
|
::Widget::TokenService.new(payload: { source_id: second_session.source_id, inbox_id: web_widget.inbox.id }).generate_token
|
||||||
|
end
|
||||||
|
|
||||||
describe 'GET /api/v1/widget/conversations' do
|
describe 'GET /api/v1/widget/conversations' do
|
||||||
context 'with a conversation' do
|
context 'with a conversation' do
|
||||||
|
@ -142,5 +146,35 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when end conversation is not permitted' do
|
||||||
|
before do
|
||||||
|
web_widget.end_conversation = false
|
||||||
|
web_widget.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns action not permitted status' do
|
||||||
|
expect(conversation.open?).to be true
|
||||||
|
|
||||||
|
get '/api/v1/widget/conversations/toggle_status',
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
params: { website_token: web_widget.website_token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(conversation.reload.resolved?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a token without any conversation is used' do
|
||||||
|
it 'returns not found status' do
|
||||||
|
get '/api/v1/widget/conversations/toggle_status',
|
||||||
|
headers: { 'X-Auth-Token' => token_without_conversation },
|
||||||
|
params: { website_token: web_widget.website_token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,8 +9,8 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
||||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||||
let(:token) { ::Widget::TokenService.new(payload: payload).generate_token }
|
let(:token) { ::Widget::TokenService.new(payload: payload).generate_token }
|
||||||
|
|
||||||
before do
|
before do |example|
|
||||||
2.times.each { create(:message, account: account, inbox: web_widget.inbox, conversation: conversation) }
|
2.times.each { create(:message, account: account, inbox: web_widget.inbox, conversation: conversation) } unless example.metadata[:skip_before]
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /api/v1/widget/messages' do
|
describe 'GET /api/v1/widget/messages' do
|
||||||
|
@ -23,9 +23,20 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
json_response = JSON.parse(response.body)
|
json_response = JSON.parse(response.body)
|
||||||
|
|
||||||
# 2 messages created + 2 messages by the email hook
|
# 2 messages created + 2 messages by the email hook
|
||||||
expect(json_response.length).to eq(4)
|
expect(json_response['payload'].length).to eq(4)
|
||||||
|
expect(json_response['meta']).not_to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns empty messages', :skip_before do
|
||||||
|
get api_v1_widget_messages_url,
|
||||||
|
params: { website_token: web_widget.website_token },
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['payload'].length).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
15
spec/fixtures/files/non_utf_encoded_mail.eml
vendored
Normal file
15
spec/fixtures/files/non_utf_encoded_mail.eml
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
In-Reply-To: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>, <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
|
||||||
|
To: "Replies" <reply+6bdc3f4d-0bec-4515-a284-5d916fdde489@example.com>
|
||||||
|
From: "=?UTF-8?B?2YXYqtis2LEg2LPZiNmCINmG2Ko=?=" <example@gmail.com>
|
||||||
|
Date: Sun, 3 Apr 2022 11:48:20 -0700
|
||||||
|
Message-ID: <CAEjnyt3uiyywAF3SrJzEedMRV5H5XG4aYvFrGdFwxtuGoRCc0w@mail.gmail.com>
|
||||||
|
Subject: =?UTF-8?Q?=D8=A3=D9=87=D9=84=D9=8A=D9=86_=D8=B9?= =?UTF-8?Q?=D9=85=D9=8A=D9=84=D9=86=D8=A7_=D8=A7=D9=84?= =?UTF-8?Q?=D9=83=D8=B1=D9=8A=D9=85_?=
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-Disposition: inline
|
||||||
|
Precedence: bulk
|
||||||
|
X-Autoreply: yes
|
||||||
|
Auto-Submitted: auto-replied
|
||||||
|
|
||||||
|
2KPZhti42LHZiNin2Iwg2KPZhtinINij2K3Yqtin2KzZh9inINmB2YLYtyDZhNiq2YLZiNmFINio2KfZhNiq2K/ZgtmK2YIg2YHZiiDZhdmC2KfZhNiq2Yog2KfZhNi02K7YtdmK2KkK
|
|
@ -24,7 +24,9 @@ describe ActionCableListener do
|
||||||
expect(conversation.inbox.reload.inbox_members.count).to eq(1)
|
expect(conversation.inbox.reload.inbox_members.count).to eq(1)
|
||||||
|
|
||||||
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
||||||
[agent.pubsub_token, admin.pubsub_token, conversation.contact_inbox.pubsub_token],
|
a_collection_containing_exactly(
|
||||||
|
agent.pubsub_token, admin.pubsub_token, conversation.contact_inbox.pubsub_token
|
||||||
|
),
|
||||||
'message.created',
|
'message.created',
|
||||||
message.push_event_data.merge(account_id: account.id)
|
message.push_event_data.merge(account_id: account.id)
|
||||||
)
|
)
|
||||||
|
@ -40,7 +42,9 @@ describe ActionCableListener do
|
||||||
verified_contact_inbox = create(:contact_inbox, contact: conversation.contact, inbox: inbox, hmac_verified: true)
|
verified_contact_inbox = create(:contact_inbox, contact: conversation.contact, inbox: inbox, hmac_verified: true)
|
||||||
|
|
||||||
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
||||||
[agent.pubsub_token, admin.pubsub_token, conversation.contact_inbox.pubsub_token, verified_contact_inbox.pubsub_token],
|
a_collection_containing_exactly(
|
||||||
|
agent.pubsub_token, admin.pubsub_token, conversation.contact_inbox.pubsub_token, verified_contact_inbox.pubsub_token
|
||||||
|
),
|
||||||
'message.created',
|
'message.created',
|
||||||
message.push_event_data.merge(account_id: account.id)
|
message.push_event_data.merge(account_id: account.id)
|
||||||
)
|
)
|
||||||
|
@ -56,7 +60,9 @@ describe ActionCableListener do
|
||||||
# HACK: to reload conversation inbox members
|
# HACK: to reload conversation inbox members
|
||||||
expect(conversation.inbox.reload.inbox_members.count).to eq(1)
|
expect(conversation.inbox.reload.inbox_members.count).to eq(1)
|
||||||
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
||||||
[admin.pubsub_token, conversation.contact.pubsub_token],
|
a_collection_containing_exactly(
|
||||||
|
admin.pubsub_token, conversation.contact.pubsub_token
|
||||||
|
),
|
||||||
'conversation.typing_on', conversation: conversation.push_event_data,
|
'conversation.typing_on', conversation: conversation.push_event_data,
|
||||||
user: agent.push_event_data,
|
user: agent.push_event_data,
|
||||||
account_id: account.id,
|
account_id: account.id,
|
||||||
|
@ -74,7 +80,9 @@ describe ActionCableListener do
|
||||||
# HACK: to reload conversation inbox members
|
# HACK: to reload conversation inbox members
|
||||||
expect(conversation.inbox.reload.inbox_members.count).to eq(1)
|
expect(conversation.inbox.reload.inbox_members.count).to eq(1)
|
||||||
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
||||||
[admin.pubsub_token, conversation.contact.pubsub_token],
|
a_collection_containing_exactly(
|
||||||
|
admin.pubsub_token, conversation.contact.pubsub_token
|
||||||
|
),
|
||||||
'conversation.typing_off', conversation: conversation.push_event_data,
|
'conversation.typing_off', conversation: conversation.push_event_data,
|
||||||
user: agent.push_event_data,
|
user: agent.push_event_data,
|
||||||
account_id: account.id,
|
account_id: account.id,
|
||||||
|
@ -91,7 +99,9 @@ describe ActionCableListener do
|
||||||
|
|
||||||
it 'sends message to account admins, inbox agents' do
|
it 'sends message to account admins, inbox agents' do
|
||||||
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
|
||||||
[agent.pubsub_token, admin.pubsub_token],
|
a_collection_containing_exactly(
|
||||||
|
agent.pubsub_token, admin.pubsub_token
|
||||||
|
),
|
||||||
'contact.deleted',
|
'contact.deleted',
|
||||||
contact.push_event_data.merge(account_id: account.id)
|
contact.push_event_data.merge(account_id: account.id)
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,6 +5,7 @@ RSpec.describe MailPresenter do
|
||||||
describe 'parsed mail decorator' do
|
describe 'parsed mail decorator' do
|
||||||
let(:mail) { create_inbound_email_from_fixture('welcome.eml').mail }
|
let(:mail) { create_inbound_email_from_fixture('welcome.eml').mail }
|
||||||
let(:html_mail) { create_inbound_email_from_fixture('welcome_html.eml').mail }
|
let(:html_mail) { create_inbound_email_from_fixture('welcome_html.eml').mail }
|
||||||
|
let(:ascii_mail) { create_inbound_email_from_fixture('non_utf_encoded_mail.eml').mail }
|
||||||
let(:decorated_mail) { described_class.new(mail) }
|
let(:decorated_mail) { described_class.new(mail) }
|
||||||
|
|
||||||
let(:mail_with_no_subject) { create_inbound_email_from_fixture('mail_with_no_subject.eml').mail }
|
let(:mail_with_no_subject) { create_inbound_email_from_fixture('mail_with_no_subject.eml').mail }
|
||||||
|
@ -65,5 +66,13 @@ RSpec.describe MailPresenter do
|
||||||
"I'm learning English as a first language for the past 13 years, but to "
|
"I'm learning English as a first language for the past 13 years, but to "
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'encodes email to UTF-8' do
|
||||||
|
decorated_html_mail = described_class.new(ascii_mail)
|
||||||
|
expect(decorated_html_mail.subject).to eq('أهلين عميلنا الكريم ')
|
||||||
|
expect(decorated_html_mail.text_content[:reply][0..70]).to eq(
|
||||||
|
'أنظروا، أنا أحتاجها فقط لتقوم بالتدقيق في مقالتي الشخصية'
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -53,18 +53,18 @@ x-tagGroups:
|
||||||
- name: Application
|
- name: Application
|
||||||
tags:
|
tags:
|
||||||
- Account AgentBots
|
- Account AgentBots
|
||||||
- Agent
|
- Agents
|
||||||
- Canned Response
|
- Canned Responses
|
||||||
- Contact
|
- Contacts
|
||||||
- Conversation
|
- Conversations
|
||||||
- Conversation Assignment
|
- Conversation Assignment
|
||||||
- Conversation Labels
|
- Conversation Labels
|
||||||
- Inbox
|
- Inboxes
|
||||||
- Messages
|
- Messages
|
||||||
- Integrations
|
- Integrations
|
||||||
- Profile
|
- Profile
|
||||||
- Teams
|
- Teams
|
||||||
- Custom Filter
|
- Custom Filters
|
||||||
- Reports
|
- Reports
|
||||||
- name: Client
|
- name: Client
|
||||||
tags:
|
tags:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
in: path
|
in: path
|
||||||
name: id
|
name: team_id
|
||||||
type: integer
|
type: integer
|
||||||
required: true
|
required: true
|
||||||
description: The ID of the team to be updated
|
description: The ID of the team to be updated
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Agent
|
- Agents
|
||||||
operationId: add-new-agent-to-account
|
operationId: add-new-agent-to-account
|
||||||
summary: Add a New Agent
|
summary: Add a New Agent
|
||||||
description: Add a new Agent to Account
|
description: Add a new Agent to Account
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Agent
|
- Agents
|
||||||
operationId: delete-agent-from-account
|
operationId: delete-agent-from-account
|
||||||
summary: Remove an Agent from Account
|
summary: Remove an Agent from Account
|
||||||
description: Remove an Agent from Account
|
description: Remove an Agent from Account
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Agent
|
- Agents
|
||||||
operationId: get-account-agents
|
operationId: get-account-agents
|
||||||
summary: List Agents in Account
|
summary: List Agents in Account
|
||||||
description: Get Details of Agents in an Account
|
description: Get Details of Agents in an Account
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Agent
|
- Agents
|
||||||
operationId: update-agent-in-account
|
operationId: update-agent-in-account
|
||||||
summary: Update Agent in Account
|
summary: Update Agent in Account
|
||||||
description: Update an Agent in Account
|
description: Update an Agent in Account
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Canned Response
|
- Canned Responses
|
||||||
operationId: add-new-canned-response-to-account
|
operationId: add-new-canned-response-to-account
|
||||||
summary: Add a New Canned Response
|
summary: Add a New Canned Response
|
||||||
description: Add a new Canned Response to Account
|
description: Add a new Canned Response to Account
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Canned Response
|
- Canned Responses
|
||||||
operationId: delete-canned-response-from-account
|
operationId: delete-canned-response-from-account
|
||||||
summary: Remove a Canned Response from Account
|
summary: Remove a Canned Response from Account
|
||||||
description: Remove a Canned Response from Account
|
description: Remove a Canned Response from Account
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Canned Response
|
- Canned Responses
|
||||||
operationId: get-account-canned-response
|
operationId: get-account-canned-response
|
||||||
summary: List all Canned Responses in an Account
|
summary: List all Canned Responses in an Account
|
||||||
description: Get Details of Canned Responses in an Account
|
description: Get Details of Canned Responses in an Account
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Contact
|
- Contacts
|
||||||
operationId: contactConversations
|
operationId: contactConversations
|
||||||
summary: Contact Conversations
|
summary: Contact Conversations
|
||||||
description: Get conversations associated to that contact
|
description: Get conversations associated to that contact
|
||||||
|
|
|
@ -8,7 +8,7 @@ parameters:
|
||||||
|
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Contact
|
- Contacts
|
||||||
operationId: contactDetails
|
operationId: contactDetails
|
||||||
summary: Show Contact
|
summary: Show Contact
|
||||||
description: Get a contact belonging to the account using ID
|
description: Get a contact belonging to the account using ID
|
||||||
|
@ -24,7 +24,7 @@ get:
|
||||||
|
|
||||||
put:
|
put:
|
||||||
tags:
|
tags:
|
||||||
- Contact
|
- Contacts
|
||||||
operationId: contactUpdate
|
operationId: contactUpdate
|
||||||
summary: Update Contact
|
summary: Update Contact
|
||||||
description: Update a contact belonging to the account using ID
|
description: Update a contact belonging to the account using ID
|
||||||
|
@ -46,7 +46,7 @@ put:
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
tags:
|
tags:
|
||||||
- Contact
|
- Contacts
|
||||||
operationId: contactDelete
|
operationId: contactDelete
|
||||||
summary: Delete Contact
|
summary: Delete Contact
|
||||||
responses:
|
responses:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Contact
|
- Contacts
|
||||||
operationId: contactFilter
|
operationId: contactFilter
|
||||||
description: Filter contacts with custom filter options and pagination
|
description: Filter contacts with custom filter options and pagination
|
||||||
summary: Contact Filter
|
summary: Contact Filter
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Contact
|
- Contacts
|
||||||
operationId: contactList
|
operationId: contactList
|
||||||
description: Listing all the resolved contacts with pagination (Page size = 15) . Resolved contacts are the ones with a value for identifier, email or phone number
|
description: Listing all the resolved contacts with pagination (Page size = 15) . Resolved contacts are the ones with a value for identifier, email or phone number
|
||||||
summary: List Contacts
|
summary: List Contacts
|
||||||
|
@ -20,7 +20,7 @@ get:
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- Contact
|
- Contacts
|
||||||
operationId: contactCreate
|
operationId: contactCreate
|
||||||
description: Create a new Contact
|
description: Create a new Contact
|
||||||
summary: Create Contact
|
summary: Create Contact
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Contact
|
- Contacts
|
||||||
operationId: contactSearch
|
operationId: contactSearch
|
||||||
description: Search the resolved contacts using a search key, currently supports email search (Page size = 15). Resolved contacts are the ones with a value for identifier, email or phone number
|
description: Search the resolved contacts using a search key, currently supports email search (Page size = 15). Resolved contacts are the ones with a value for identifier, email or phone number
|
||||||
summary: Search Contacts
|
summary: Search Contacts
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Conversation
|
- Conversations
|
||||||
operationId: conversationFilter
|
operationId: conversationFilter
|
||||||
description: Filter conversations with custom filter options and pagination
|
description: Filter conversations with custom filter options and pagination
|
||||||
summary: Conversations Filter
|
summary: Conversations Filter
|
||||||
|
|
|
@ -3,7 +3,7 @@ parameters:
|
||||||
|
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Conversation
|
- Conversations
|
||||||
operationId: conversationList
|
operationId: conversationList
|
||||||
description: List all the conversations with pagination
|
description: List all the conversations with pagination
|
||||||
summary: Conversations List
|
summary: Conversations List
|
||||||
|
@ -43,7 +43,7 @@ get:
|
||||||
|
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- Conversation
|
- Conversations
|
||||||
operationId: newConversation
|
operationId: newConversation
|
||||||
summary: Create New Conversation
|
summary: Create New Conversation
|
||||||
description: "Creating a conversation in chatwoot requires a source id. \n\n Learn more about source_id: https://github.com/chatwoot/chatwoot/wiki/Building-on-Top-of-Chatwoot:-Importing-Existing-Contacts-and-Creating-Conversations"
|
description: "Creating a conversation in chatwoot requires a source id. \n\n Learn more about source_id: https://github.com/chatwoot/chatwoot/wiki/Building-on-Top-of-Chatwoot:-Importing-Existing-Contacts-and-Creating-Conversations"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Conversation
|
- Conversations
|
||||||
operationId: get-details-of-a-conversation
|
operationId: get-details-of-a-conversation
|
||||||
summary: Conversation Details
|
summary: Conversation Details
|
||||||
description: Get all details regarding a conversation with all messages in the conversation
|
description: Get all details regarding a conversation with all messages in the conversation
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Conversation
|
- Conversations
|
||||||
operationId: toggle-status-of-a-conversation
|
operationId: toggle-status-of-a-conversation
|
||||||
summary: Toggle Status
|
summary: Toggle Status
|
||||||
description: Toggles the status of the conversation between open and resolved
|
description: Toggles the status of the conversation between open and resolved
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- Conversation
|
- Conversations
|
||||||
operationId: conversationUpdateLastSeen
|
operationId: conversationUpdateLastSeen
|
||||||
summary: Update Last Seen
|
summary: Update Last Seen
|
||||||
description: Updates the last seen of the conversation so that conversations will have the bubbles in the agents screen
|
description: Updates the last seen of the conversation so that conversations will have the bubbles in the agents screen
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Custom Filter
|
- Custom Filters
|
||||||
operationId: create-a-custom-filter
|
operationId: create-a-custom-filter
|
||||||
summary: Create a custom filter
|
summary: Create a custom filter
|
||||||
description: Create a custom filter in the account
|
description: Create a custom filter in the account
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Custom Filter
|
- Custom Filters
|
||||||
operationId: delete-a-custom-filter
|
operationId: delete-a-custom-filter
|
||||||
summary: Delete a custom filter
|
summary: Delete a custom filter
|
||||||
description: Delete a custom filter from the account
|
description: Delete a custom filter from the account
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Custom Filter
|
- Custom Filters
|
||||||
operationId: list-all-filters
|
operationId: list-all-filters
|
||||||
summary: List all custom filters
|
summary: List all custom filters
|
||||||
description: List all custom filters in a category of a user
|
description: List all custom filters in a category of a user
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Custom Filter
|
- Custom Filters
|
||||||
operationId: get-details-of-a-single-custom-filter
|
operationId: get-details-of-a-single-custom-filter
|
||||||
summary: Get a custom filter details
|
summary: Get a custom filter details
|
||||||
description: Get the details of a custom filter in the account
|
description: Get the details of a custom filter in the account
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
tags:
|
tags:
|
||||||
- Custom Filter
|
- Custom Filters
|
||||||
operationId: update-a-custom-filter
|
operationId: update-a-custom-filter
|
||||||
summary: Update a custom filter
|
summary: Update a custom filter
|
||||||
description: Update a custom filter's attributes
|
description: Update a custom filter's attributes
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- Inbox
|
- Inboxes
|
||||||
operationId: inboxCreation
|
operationId: inboxCreation
|
||||||
summary: Create an inbox
|
summary: Create an inbox
|
||||||
description: You can create more than one website inbox in each account
|
description: You can create more than one website inbox in each account
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue