Merge branch 'develop' into feat/add_lograge
This commit is contained in:
commit
a4e1730297
72 changed files with 599 additions and 182 deletions
|
@ -25,7 +25,7 @@ class NotificationSubscriptionBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_identifier_subscription
|
def build_identifier_subscription
|
||||||
@identifier_subscription = user.notification_subscriptions.create(params.merge(identifier: identifier))
|
@identifier_subscription = user.notification_subscriptions.create!(params.merge(identifier: identifier))
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_identifier_subscription
|
def update_identifier_subscription
|
||||||
|
|
|
@ -49,7 +49,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||||
def clone
|
def clone
|
||||||
automation_rule = Current.account.automation_rules.find_by(id: params[:automation_rule_id])
|
automation_rule = Current.account.automation_rules.find_by(id: params[:automation_rule_id])
|
||||||
new_rule = automation_rule.dup
|
new_rule = automation_rule.dup
|
||||||
new_rule.save
|
new_rule.save!
|
||||||
@automation_rule = new_rule
|
@automation_rule = new_rule
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||||
phone_number: phone_number,
|
phone_number: phone_number,
|
||||||
medium: medium
|
medium: medium
|
||||||
)
|
)
|
||||||
@inbox = Current.account.inboxes.create(
|
@inbox = Current.account.inboxes.create!(
|
||||||
name: permitted_params[:name],
|
name: permitted_params[:name],
|
||||||
channel: @twilio_channel
|
channel: @twilio_channel
|
||||||
)
|
)
|
||||||
|
|
|
@ -135,7 +135,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||||
source_id = params[:source_id] || SecureRandom.uuid
|
source_id = params[:source_id] || SecureRandom.uuid
|
||||||
ContactInbox.create(contact: @contact, inbox: inbox, source_id: source_id)
|
ContactInbox.create!(contact: @contact, inbox: inbox, source_id: source_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
process_update_contact
|
process_update_contact
|
||||||
@conversation = create_conversation
|
@conversation = create_conversation
|
||||||
conversation.messages.create(message_params)
|
conversation.messages.create!(message_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
|
|
||||||
unless conversation.resolved?
|
unless conversation.resolved?
|
||||||
conversation.status = :resolved
|
conversation.status = :resolved
|
||||||
conversation.save
|
conversation.save!
|
||||||
end
|
end
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,7 +23,7 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
|
||||||
|
|
||||||
def authenticate_resource_with_sso_token
|
def authenticate_resource_with_sso_token
|
||||||
@token = @resource.create_token
|
@token = @resource.create_token
|
||||||
@resource.save
|
@resource.save!
|
||||||
|
|
||||||
sign_in(:user, @resource, store: false, bypass: false)
|
sign_in(:user, @resource, store: false, bypass: false)
|
||||||
# invalidate the token after the user is signed in
|
# invalidate the token after the user is signed in
|
||||||
|
|
|
@ -3,6 +3,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
|
||||||
before_action :set_portal
|
before_action :set_portal
|
||||||
before_action :set_category
|
before_action :set_category
|
||||||
before_action :set_article, only: [:show]
|
before_action :set_article, only: [:show]
|
||||||
|
layout 'portal'
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@articles = @portal.articles
|
@articles = @portal.articles
|
||||||
|
@ -15,6 +16,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
|
||||||
|
|
||||||
def set_article
|
def set_article
|
||||||
@article = @category.articles.find(params[:id])
|
@article = @category.articles.find(params[:id])
|
||||||
|
@parsed_content = render_article_content(@article.content)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_category
|
def set_category
|
||||||
|
@ -28,4 +30,10 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
|
||||||
def list_params
|
def list_params
|
||||||
params.permit(:query)
|
params.permit(:query)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_article_content(content)
|
||||||
|
# rubocop:disable Rails/OutputSafety
|
||||||
|
CommonMarker.render_html(content).html_safe
|
||||||
|
# rubocop:enable Rails/OutputSafety
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@ class Public::Api::V1::Portals::CategoriesController < PublicController
|
||||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||||
before_action :set_portal
|
before_action :set_portal
|
||||||
before_action :set_category, only: [:show]
|
before_action :set_category, only: [:show]
|
||||||
|
layout 'portal'
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@categories = @portal.categories
|
@categories = @portal.categories
|
||||||
|
@ -12,7 +13,7 @@ class Public::Api::V1::Portals::CategoriesController < PublicController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_category
|
def set_category
|
||||||
@category = @portal.categories.find_by!(locale: params[:locale])
|
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_portal
|
def set_portal
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
class Public::Api::V1::PortalsController < PublicController
|
class Public::Api::V1::PortalsController < PublicController
|
||||||
before_action :ensure_custom_domain_request, only: [:show]
|
before_action :ensure_custom_domain_request, only: [:show]
|
||||||
before_action :set_portal
|
before_action :set_portal
|
||||||
|
layout 'portal'
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
|
|
|
@ -44,12 +44,12 @@ class Twitter::CallbacksController < Twitter::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_inbox
|
def create_inbox
|
||||||
twitter_profile = account.twitter_profiles.create(
|
twitter_profile = account.twitter_profiles.create!(
|
||||||
twitter_access_token: parsed_body['oauth_token'],
|
twitter_access_token: parsed_body['oauth_token'],
|
||||||
twitter_access_token_secret: parsed_body['oauth_token_secret'],
|
twitter_access_token_secret: parsed_body['oauth_token_secret'],
|
||||||
profile_id: parsed_body['user_id']
|
profile_id: parsed_body['user_id']
|
||||||
)
|
)
|
||||||
account.inboxes.create(
|
account.inboxes.create!(
|
||||||
name: parsed_body['screen_name'],
|
name: parsed_body['screen_name'],
|
||||||
channel: twitter_profile
|
channel: twitter_profile
|
||||||
)
|
)
|
||||||
|
|
|
@ -285,8 +285,6 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-menu .nested.vertical.menu {
|
.secondary-menu .nested.vertical.menu {
|
||||||
overflow-y: auto;
|
|
||||||
height: 100%;
|
|
||||||
margin-left: var(--space-small);
|
margin-left: var(--space-small);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -245,6 +245,8 @@ export default {
|
||||||
@import '~dashboard/assets/scss/woot';
|
@import '~dashboard/assets/scss/woot';
|
||||||
|
|
||||||
.secondary-menu {
|
.secondary-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
border-right: 1px solid var(--s-50);
|
border-right: 1px solid var(--s-50);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -267,7 +269,6 @@ export default {
|
||||||
.menu {
|
.menu {
|
||||||
padding: var(--space-small);
|
padding: var(--space-small);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: 94%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -37,17 +37,6 @@
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</file-upload>
|
</file-upload>
|
||||||
<woot-button
|
|
||||||
v-if="enableRichEditor && !isOnPrivateNote"
|
|
||||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
|
||||||
icon="quote"
|
|
||||||
emoji="🖊️"
|
|
||||||
color-scheme="secondary"
|
|
||||||
variant="smooth"
|
|
||||||
size="small"
|
|
||||||
:title="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
|
||||||
@click="toggleFormatMode"
|
|
||||||
/>
|
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="showAudioRecorderButton"
|
v-if="showAudioRecorderButton"
|
||||||
:icon="!isRecordingAudio ? 'microphone' : 'microphone-off'"
|
:icon="!isRecordingAudio ? 'microphone' : 'microphone-off'"
|
||||||
|
@ -128,10 +117,7 @@
|
||||||
<script>
|
<script>
|
||||||
import FileUpload from 'vue-upload-component';
|
import FileUpload from 'vue-upload-component';
|
||||||
import * as ActiveStorage from 'activestorage';
|
import * as ActiveStorage from 'activestorage';
|
||||||
import {
|
import { hasPressedAltAndAKey } from 'shared/helpers/KeyboardHelpers';
|
||||||
hasPressedAltAndWKey,
|
|
||||||
hasPressedAltAndAKey,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
|
@ -207,10 +193,6 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
setFormatMode: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
isFormatMode: {
|
isFormatMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -219,10 +201,6 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
enableRichEditor: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
enterToSendEnabled: {
|
enterToSendEnabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
@ -296,16 +274,10 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleKeyEvents(e) {
|
handleKeyEvents(e) {
|
||||||
if (hasPressedAltAndWKey(e)) {
|
|
||||||
this.toggleFormatMode();
|
|
||||||
}
|
|
||||||
if (hasPressedAltAndAKey(e)) {
|
if (hasPressedAltAndAKey(e)) {
|
||||||
this.$refs.upload.$children[1].$el.click();
|
this.$refs.upload.$children[1].$el.click();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleFormatMode() {
|
|
||||||
this.setFormatMode(!this.isFormatMode);
|
|
||||||
},
|
|
||||||
toggleEnterToSend() {
|
toggleEnterToSend() {
|
||||||
this.$emit('toggleEnterToSend', !this.enterToSendEnabled);
|
this.$emit('toggleEnterToSend', !this.enterToSendEnabled);
|
||||||
},
|
},
|
||||||
|
|
|
@ -107,10 +107,8 @@
|
||||||
:recording-audio-duration-text="recordingAudioDurationText"
|
:recording-audio-duration-text="recordingAudioDurationText"
|
||||||
:recording-audio-state="recordingAudioState"
|
:recording-audio-state="recordingAudioState"
|
||||||
:is-recording-audio="isRecordingAudio"
|
:is-recording-audio="isRecordingAudio"
|
||||||
:set-format-mode="setFormatMode"
|
|
||||||
:is-on-private-note="isOnPrivateNote"
|
:is-on-private-note="isOnPrivateNote"
|
||||||
:is-format-mode="showRichContentEditor"
|
:is-format-mode="showRichContentEditor"
|
||||||
:enable-rich-editor="isRichEditorEnabled"
|
|
||||||
:enter-to-send-enabled="enterToSendEnabled"
|
:enter-to-send-enabled="enterToSendEnabled"
|
||||||
:enable-multiple-file-upload="enableMultipleFileUpload"
|
:enable-multiple-file-upload="enableMultipleFileUpload"
|
||||||
:has-whatsapp-templates="hasWhatsappTemplates"
|
:has-whatsapp-templates="hasWhatsappTemplates"
|
||||||
|
@ -229,17 +227,10 @@ export default {
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
}),
|
}),
|
||||||
showRichContentEditor() {
|
showRichContentEditor() {
|
||||||
if (this.isOnPrivateNote) {
|
if (this.isOnPrivateNote || this.isRichEditorEnabled) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isRichEditorEnabled) {
|
|
||||||
const {
|
|
||||||
display_rich_content_editor: displayRichContentEditor,
|
|
||||||
} = this.uiSettings;
|
|
||||||
|
|
||||||
return displayRichContentEditor;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
assignedAgent: {
|
assignedAgent: {
|
||||||
|
@ -375,7 +366,7 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
isRichEditorEnabled() {
|
isRichEditorEnabled() {
|
||||||
return this.isAWebWidgetInbox || this.isAnEmailChannel;
|
return this.isAWebWidgetInbox || this.isAnEmailChannel || this.isAPIInbox;
|
||||||
},
|
},
|
||||||
showAudioRecorder() {
|
showAudioRecorder() {
|
||||||
return !this.isOnPrivateNote && this.showFileUpload;
|
return !this.isOnPrivateNote && this.showFileUpload;
|
||||||
|
@ -799,9 +790,6 @@ export default {
|
||||||
|
|
||||||
return messagePayload;
|
return messagePayload;
|
||||||
},
|
},
|
||||||
setFormatMode(value) {
|
|
||||||
this.updateUISettings({ display_rich_content_editor: value });
|
|
||||||
},
|
|
||||||
setCcEmails(value) {
|
setCcEmails(value) {
|
||||||
this.bccEmails = value.bccEmails;
|
this.bccEmails = value.bccEmails;
|
||||||
this.ccEmails = value.ccEmails;
|
this.ccEmails = value.ccEmails;
|
||||||
|
|
|
@ -65,12 +65,6 @@ export const SHORTCUT_KEYS = [
|
||||||
firstkey: 'Alt / ⌥',
|
firstkey: 'Alt / ⌥',
|
||||||
secondKey: 'P',
|
secondKey: 'P',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 12,
|
|
||||||
label: 'TOGGLE_RICH_CONTENT_EDITOR',
|
|
||||||
firstkey: 'Alt / ⌥',
|
|
||||||
secondKey: 'W',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 13,
|
id: 13,
|
||||||
label: 'SWITCH_TO_REPLY',
|
label: 'SWITCH_TO_REPLY',
|
||||||
|
|
|
@ -253,7 +253,6 @@
|
||||||
"GO_TO_SETTINGS": "Go to Settings",
|
"GO_TO_SETTINGS": "Go to Settings",
|
||||||
"SWITCH_CONVERSATION_STATUS": "Switch to the next conversation status",
|
"SWITCH_CONVERSATION_STATUS": "Switch to the next conversation status",
|
||||||
"SWITCH_TO_PRIVATE_NOTE": "Switch to Private Note",
|
"SWITCH_TO_PRIVATE_NOTE": "Switch to Private Note",
|
||||||
"TOGGLE_RICH_CONTENT_EDITOR": "Toggle Rich Content editor",
|
|
||||||
"SWITCH_TO_REPLY": "Switch to Reply",
|
"SWITCH_TO_REPLY": "Switch to Reply",
|
||||||
"TOGGLE_SNOOZE_DROPDOWN": "Toggle snooze dropdown"
|
"TOGGLE_SNOOZE_DROPDOWN": "Toggle snooze dropdown"
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,6 @@ describe('uiSettingsMixin', () => {
|
||||||
actions = { updateUISettings: jest.fn(), toggleSidebarUIState: jest.fn() };
|
actions = { updateUISettings: jest.fn(), toggleSidebarUIState: jest.fn() };
|
||||||
getters = {
|
getters = {
|
||||||
getUISettings: () => ({
|
getUISettings: () => ({
|
||||||
display_rich_content_editor: false,
|
|
||||||
enter_to_send_enabled: false,
|
enter_to_send_enabled: false,
|
||||||
is_ct_labels_open: true,
|
is_ct_labels_open: true,
|
||||||
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
|
@ -34,7 +33,6 @@ describe('uiSettingsMixin', () => {
|
||||||
};
|
};
|
||||||
const wrapper = shallowMount(Component, { store, localVue });
|
const wrapper = shallowMount(Component, { store, localVue });
|
||||||
expect(wrapper.vm.uiSettings).toEqual({
|
expect(wrapper.vm.uiSettings).toEqual({
|
||||||
display_rich_content_editor: false,
|
|
||||||
enter_to_send_enabled: false,
|
enter_to_send_enabled: false,
|
||||||
is_ct_labels_open: true,
|
is_ct_labels_open: true,
|
||||||
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
|
@ -55,7 +53,6 @@ describe('uiSettingsMixin', () => {
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
{
|
{
|
||||||
uiSettings: {
|
uiSettings: {
|
||||||
display_rich_content_editor: false,
|
|
||||||
enter_to_send_enabled: true,
|
enter_to_send_enabled: true,
|
||||||
is_ct_labels_open: true,
|
is_ct_labels_open: true,
|
||||||
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
|
@ -80,7 +77,6 @@ describe('uiSettingsMixin', () => {
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
{
|
{
|
||||||
uiSettings: {
|
uiSettings: {
|
||||||
display_rich_content_editor: false,
|
|
||||||
enter_to_send_enabled: false,
|
enter_to_send_enabled: false,
|
||||||
is_ct_labels_open: false,
|
is_ct_labels_open: false,
|
||||||
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="table-actions-wrap">
|
<div class="table-actions-wrap">
|
||||||
<div class="left-aligned-wrap">
|
<div class="left-aligned-wrap">
|
||||||
|
<woot-sidemenu-icon />
|
||||||
<h1 class="page-title">
|
<h1 class="page-title">
|
||||||
{{ headerTitle }}
|
{{ headerTitle }}
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -173,11 +174,13 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-wrap {
|
.search-wrap {
|
||||||
width: 400px;
|
max-width: 400px;
|
||||||
|
min-width: 150px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-right: var(--space-small);
|
margin-right: var(--space-small);
|
||||||
|
margin-left: var(--space-small);
|
||||||
|
|
||||||
.search-icon {
|
.search-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<woot-button
|
<woot-button
|
||||||
icon="chevron-left"
|
icon="chevron-left"
|
||||||
variant="clear"
|
variant="clear"
|
||||||
|
size="small"
|
||||||
color-scheme="primary"
|
color-scheme="primary"
|
||||||
@click="onClickGoBack"
|
@click="onClickGoBack"
|
||||||
>
|
>
|
||||||
|
|
|
@ -6,17 +6,16 @@
|
||||||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||||
/>
|
/>
|
||||||
<div v-if="portals.length">
|
<help-center-sidebar
|
||||||
<help-center-sidebar
|
v-if="portals.length"
|
||||||
:class="sidebarClassName"
|
:class="sidebarClassName"
|
||||||
:header-title="headerTitle"
|
:header-title="headerTitle"
|
||||||
:sub-title="localeName(selectedPortalLocale)"
|
:sub-title="localeName(selectedLocaleInPortal)"
|
||||||
:accessible-menu-items="accessibleMenuItems"
|
:accessible-menu-items="accessibleMenuItems"
|
||||||
:additional-secondary-menu-items="additionalSecondaryMenuItems"
|
:additional-secondary-menu-items="additionalSecondaryMenuItems"
|
||||||
@open-popover="openPortalPopover"
|
@open-popover="openPortalPopover"
|
||||||
@open-modal="onClickOpenAddCatogoryModal"
|
@open-modal="onClickOpenAddCatogoryModal"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<section class="app-content columns" :class="contentClassName">
|
<section class="app-content columns" :class="contentClassName">
|
||||||
<router-view />
|
<router-view />
|
||||||
<command-bar />
|
<command-bar />
|
||||||
|
@ -33,13 +32,15 @@
|
||||||
v-if="showPortalPopover"
|
v-if="showPortalPopover"
|
||||||
:portals="portals"
|
:portals="portals"
|
||||||
:active-portal-slug="selectedPortalSlug"
|
:active-portal-slug="selectedPortalSlug"
|
||||||
|
:active-locale="selectedLocaleInPortal"
|
||||||
@close-popover="closePortalPopover"
|
@close-popover="closePortalPopover"
|
||||||
/>
|
/>
|
||||||
<add-category
|
<add-category
|
||||||
v-if="showAddCategoryModal"
|
v-if="showAddCategoryModal"
|
||||||
:show.sync="showAddCategoryModal"
|
:show.sync="showAddCategoryModal"
|
||||||
:portal-name="selectedPortalName"
|
:portal-name="selectedPortalName"
|
||||||
:locale="selectedPortalLocale"
|
:locale="selectedLocaleInPortal"
|
||||||
|
:portal-slug="selectedPortalSlug"
|
||||||
@cancel="onClickCloseAddCategoryModal"
|
@cancel="onClickCloseAddCategoryModal"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
@ -96,6 +97,9 @@ export default {
|
||||||
|
|
||||||
return this.$store.getters['portals/allPortals'][0];
|
return this.$store.getters['portals/allPortals'][0];
|
||||||
},
|
},
|
||||||
|
selectedLocaleInPortal() {
|
||||||
|
return this.$route.params.locale || this.defaultPortalLocale;
|
||||||
|
},
|
||||||
sidebarClassName() {
|
sidebarClassName() {
|
||||||
if (this.isOnDesktop) {
|
if (this.isOnDesktop) {
|
||||||
return '';
|
return '';
|
||||||
|
@ -120,7 +124,7 @@ export default {
|
||||||
selectedPortalSlug() {
|
selectedPortalSlug() {
|
||||||
return this.selectedPortal ? this.selectedPortal?.slug : '';
|
return this.selectedPortal ? this.selectedPortal?.slug : '';
|
||||||
},
|
},
|
||||||
selectedPortalLocale() {
|
defaultPortalLocale() {
|
||||||
return this.selectedPortal
|
return this.selectedPortal
|
||||||
? this.selectedPortal?.meta?.default_locale
|
? this.selectedPortal?.meta?.default_locale
|
||||||
: '';
|
: '';
|
||||||
|
@ -142,7 +146,7 @@ export default {
|
||||||
key: 'list_all_locale_articles',
|
key: 'list_all_locale_articles',
|
||||||
count: allArticlesCount,
|
count: allArticlesCount,
|
||||||
toState: frontendURL(
|
toState: frontendURL(
|
||||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles`
|
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedLocaleInPortal}/articles`
|
||||||
),
|
),
|
||||||
toolTip: 'All Articles',
|
toolTip: 'All Articles',
|
||||||
toStateName: 'list_all_locale_articles',
|
toStateName: 'list_all_locale_articles',
|
||||||
|
@ -153,7 +157,7 @@ export default {
|
||||||
key: 'list_mine_articles',
|
key: 'list_mine_articles',
|
||||||
count: mineArticlesCount,
|
count: mineArticlesCount,
|
||||||
toState: frontendURL(
|
toState: frontendURL(
|
||||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles/mine`
|
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedLocaleInPortal}/articles/mine`
|
||||||
),
|
),
|
||||||
toolTip: 'My articles',
|
toolTip: 'My articles',
|
||||||
toStateName: 'list_mine_articles',
|
toStateName: 'list_mine_articles',
|
||||||
|
@ -164,7 +168,7 @@ export default {
|
||||||
key: 'list_draft_articles',
|
key: 'list_draft_articles',
|
||||||
count: draftArticlesCount,
|
count: draftArticlesCount,
|
||||||
toState: frontendURL(
|
toState: frontendURL(
|
||||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles/draft`
|
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedLocaleInPortal}/articles/draft`
|
||||||
),
|
),
|
||||||
toolTip: 'Draft',
|
toolTip: 'Draft',
|
||||||
toStateName: 'list_draft_articles',
|
toStateName: 'list_draft_articles',
|
||||||
|
@ -175,7 +179,7 @@ export default {
|
||||||
key: 'list_archived_articles',
|
key: 'list_archived_articles',
|
||||||
count: archivedArticlesCount,
|
count: archivedArticlesCount,
|
||||||
toState: frontendURL(
|
toState: frontendURL(
|
||||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles/archived`
|
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedLocaleInPortal}/articles/archived`
|
||||||
),
|
),
|
||||||
toolTip: 'Archived',
|
toolTip: 'Archived',
|
||||||
toStateName: 'list_archived_articles',
|
toStateName: 'list_archived_articles',
|
||||||
|
|
|
@ -34,18 +34,11 @@
|
||||||
:key="portal.id"
|
:key="portal.id"
|
||||||
:portal="portal"
|
:portal="portal"
|
||||||
:active-portal-slug="activePortalSlug"
|
:active-portal-slug="activePortalSlug"
|
||||||
|
:active-locale="activeLocale"
|
||||||
:active="portal.slug === activePortalSlug"
|
:active="portal.slug === activePortalSlug"
|
||||||
@open-portal-page="onPortalSelect"
|
@open-portal-page="onPortalSelect"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
|
||||||
<woot-button variant="link" @click="closePortalPopover">
|
|
||||||
{{ $t('HELP_CENTER.PORTAL.POPOVER.CANCEL_BUTTON_LABEL') }}
|
|
||||||
</woot-button>
|
|
||||||
<woot-button @click="() => {}">
|
|
||||||
{{ $t('HELP_CENTER.PORTAL.POPOVER.CHOOSE_LOCALE_BUTTON') }}
|
|
||||||
</woot-button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -66,6 +59,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
activeLocale: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -125,12 +122,5 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: end;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-small);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -19,13 +19,13 @@
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div class="portal-locales">
|
<div class="portal-locales">
|
||||||
<h5 class="locale-title">
|
<h5 class="locale-title sub-block-title">
|
||||||
{{ $t('HELP_CENTER.PORTAL.CHOOSE_LOCALE_LABEL') }}
|
{{ $t('HELP_CENTER.PORTAL.CHOOSE_LOCALE_LABEL') }}
|
||||||
</h5>
|
</h5>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="locale in locales" :key="locale.code">
|
<li v-for="locale in locales" :key="locale.code">
|
||||||
<woot-button
|
<woot-button
|
||||||
:class="
|
:variant="
|
||||||
`locale-item ${
|
`locale-item ${
|
||||||
isLocaleActive(locale.code, activePortalSlug)
|
isLocaleActive(locale.code, activePortalSlug)
|
||||||
? 'smooth'
|
? 'smooth'
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
>
|
>
|
||||||
<div class="locale-content">
|
<div class="locale-content">
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<h6 class="text-block-title text-left">
|
<h6 class="text-block-title text-left locale-name">
|
||||||
<span>
|
<span>
|
||||||
{{ localeName(locale.code) }}
|
{{ localeName(locale.code) }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -90,6 +90,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
activeLocale: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -129,7 +133,7 @@ export default {
|
||||||
},
|
},
|
||||||
isLocaleActive(code, slug) {
|
isLocaleActive(code, slug) {
|
||||||
const isPortalActive = this.portal.slug === slug;
|
const isPortalActive = this.portal.slug === slug;
|
||||||
const isLocaleActive = this.portal?.meta?.default_locale === code;
|
const isLocaleActive = this.activeLocale === code;
|
||||||
return isPortalActive && isLocaleActive;
|
return isPortalActive && isLocaleActive;
|
||||||
},
|
},
|
||||||
isLocaleDefault(code) {
|
isLocaleDefault(code) {
|
||||||
|
@ -151,6 +155,7 @@ export default {
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
border: 1px solid var(--w-400);
|
border: 1px solid var(--w-400);
|
||||||
|
background: var(---25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions-container {
|
.actions-container {
|
||||||
|
@ -177,15 +182,13 @@ export default {
|
||||||
.portal-count {
|
.portal-count {
|
||||||
font-size: var(--font-size-mini);
|
font-size: var(--font-size-mini);
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
color: var(--s-500);
|
color: var(--s-600);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.portal-locales {
|
.portal-locales {
|
||||||
.locale-title {
|
.locale-name {
|
||||||
color: var(--s-600);
|
margin-bottom: var(--space-micro);
|
||||||
font-size: var(--font-size-default);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.locale-content {
|
.locale-content {
|
||||||
|
@ -204,6 +207,7 @@ export default {
|
||||||
.locale__radio {
|
.locale__radio {
|
||||||
width: var(--space-large);
|
width: var(--space-large);
|
||||||
margin-top: var(--space-tiny);
|
margin-top: var(--space-tiny);
|
||||||
|
color: var(--g-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-locale-wrap {
|
.add-locale-wrap {
|
||||||
|
@ -227,7 +231,7 @@ export default {
|
||||||
|
|
||||||
.locale-meta {
|
.locale-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--s-500);
|
color: var(--s-600);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
line-height: var(--space-normal);
|
line-height: var(--space-normal);
|
||||||
|
|
|
@ -69,7 +69,7 @@ export default {
|
||||||
},
|
},
|
||||||
portalLink() {
|
portalLink() {
|
||||||
const slug = this.$route.params.portalSlug;
|
const slug = this.$route.params.portalSlug;
|
||||||
return `/public/api/v1/portals/${slug}`;
|
return `/hc/${slug}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -89,6 +89,8 @@ export default {
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '~dashboard/assets/scss/woot';
|
@import '~dashboard/assets/scss/woot';
|
||||||
.secondary-menu {
|
.secondary-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
border-right: 1px solid var(--s-50);
|
border-right: 1px solid var(--s-50);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -108,5 +110,10 @@ export default {
|
||||||
&:hover {
|
&:hover {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
padding: var(--space-small);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -86,7 +86,7 @@ export default {
|
||||||
},
|
},
|
||||||
portalLink() {
|
portalLink() {
|
||||||
const slug = this.$route.params.portalSlug;
|
const slug = this.$route.params.portalSlug;
|
||||||
return `/public/api/v1/portals/${slug}`;
|
return `/hc/${slug}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
@ -86,6 +86,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
portalSlug: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -105,7 +109,9 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
selectedPortalSlug() {
|
selectedPortalSlug() {
|
||||||
return this.$route.params.portalSlug;
|
return this.$route.params.portalSlug
|
||||||
|
? this.$route.params.portalSlug
|
||||||
|
: this.portalSlug;
|
||||||
},
|
},
|
||||||
nameError() {
|
nameError() {
|
||||||
if (this.$v.name.$error) {
|
if (this.$v.name.$error) {
|
||||||
|
|
|
@ -140,13 +140,13 @@ export default {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 var(--space-medium);
|
padding: 0 0 0 var(--space-normal);
|
||||||
.button-container {
|
.button-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
.locale-container {
|
.locale-container {
|
||||||
margin-top: var(--space-large);
|
margin-top: var(--space-normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
16
app/javascript/packs/portal.js
Normal file
16
app/javascript/packs/portal.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// This file is automatically compiled by Webpack, along with any other files
|
||||||
|
// present in this directory. You're encouraged to place your actual application logic in
|
||||||
|
// a relevant structure within app/javascript and only use these pack files to reference
|
||||||
|
// that code so that it will be compiled.
|
||||||
|
|
||||||
|
import Rails from '@rails/ujs';
|
||||||
|
import Turbolinks from 'turbolinks';
|
||||||
|
|
||||||
|
import { navigateToLocalePage } from '../portal/portalHelpers';
|
||||||
|
|
||||||
|
import '../portal/application.scss';
|
||||||
|
|
||||||
|
Rails.start();
|
||||||
|
Turbolinks.start();
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', navigateToLocalePage);
|
78
app/javascript/portal/application.scss
Normal file
78
app/javascript/portal/application.scss
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
@import 'tailwindcss/base';
|
||||||
|
@import 'tailwindcss/components';
|
||||||
|
@import 'tailwindcss/utilities';
|
||||||
|
|
||||||
|
@import 'widget/assets/scss/reset';
|
||||||
|
@import 'widget/assets/scss/variables';
|
||||||
|
@import 'widget/assets/scss/buttons';
|
||||||
|
@import 'widget/assets/scss/mixins';
|
||||||
|
@import 'widget/assets/scss/forms';
|
||||||
|
@import 'shared/assets/fonts/widget_fonts';
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
font-family: $font-family;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woot-survey-wrap {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-content {
|
||||||
|
@apply text-lg;
|
||||||
|
@apply font-sans;
|
||||||
|
@apply text-slate-800;
|
||||||
|
@apply leading-normal;
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
@apply font-sans leading-relaxed font-extrabold text-slate-900;
|
||||||
|
@apply mb-4;
|
||||||
|
@apply mt-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
@apply text-5xl leading-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
@apply text-4xl leading-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
@apply text-3xl leading-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
@apply text-2xl leading-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
@apply text-lg;
|
||||||
|
@apply font-sans;
|
||||||
|
@apply text-slate-800;
|
||||||
|
@apply leading-relaxed;
|
||||||
|
@apply mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
@apply list-disc;
|
||||||
|
@apply pl-8;
|
||||||
|
@apply ml-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
@apply text-lg;
|
||||||
|
@apply font-sans;
|
||||||
|
@apply text-slate-800;
|
||||||
|
@apply leading-relaxed;
|
||||||
|
@apply mb-2;
|
||||||
|
}
|
||||||
|
}
|
8
app/javascript/portal/portalHelpers.js
Normal file
8
app/javascript/portal/portalHelpers.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export const navigateToLocalePage = () => {
|
||||||
|
const allLocaleSwitcher = document.querySelector('.locale-switcher');
|
||||||
|
|
||||||
|
const { portalSlug } = allLocaleSwitcher.dataset;
|
||||||
|
allLocaleSwitcher.addEventListener('change', event => {
|
||||||
|
window.location = `/hc/${portalSlug}/${event.target.value}/`;
|
||||||
|
});
|
||||||
|
};
|
23
app/javascript/portal/specs/portal.spec.js
Normal file
23
app/javascript/portal/specs/portal.spec.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { navigateToLocalePage } from '../portalHelpers';
|
||||||
|
|
||||||
|
describe('#navigateToLocalePage', () => {
|
||||||
|
it('returns correct cookie name', () => {
|
||||||
|
const elemDiv = document.createElement('div');
|
||||||
|
elemDiv.classList.add('locale-switcher');
|
||||||
|
document.body.appendChild(elemDiv);
|
||||||
|
|
||||||
|
const allLocaleSwitcher = document.querySelector('.locale-switcher');
|
||||||
|
|
||||||
|
allLocaleSwitcher.addEventListener = jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementationOnce((event, callback) => {
|
||||||
|
callback({ target: { value: 1 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
navigateToLocalePage();
|
||||||
|
expect(allLocaleSwitcher.addEventListener).toBeCalledWith(
|
||||||
|
'change',
|
||||||
|
expect.any(Function)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -42,10 +42,6 @@ export const hasPressedAltAndNKey = e => {
|
||||||
return e.altKey && e.keyCode === 78;
|
return e.altKey && e.keyCode === 78;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasPressedAltAndWKey = e => {
|
|
||||||
return e.altKey && e.keyCode === 87;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasPressedAltAndAKey = e => {
|
export const hasPressedAltAndAKey = e => {
|
||||||
return e.altKey && e.keyCode === 65;
|
return e.altKey && e.keyCode === 65;
|
||||||
};
|
};
|
||||||
|
|
|
@ -34,11 +34,14 @@ export const hasEmojiSupport = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeEmoji = text => {
|
export const removeEmoji = text => {
|
||||||
return text
|
if (text) {
|
||||||
.replace(
|
return text
|
||||||
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
|
.replace(
|
||||||
''
|
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
|
||||||
)
|
''
|
||||||
.replace(/\s+/g, ' ')
|
)
|
||||||
.trim();
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,6 @@ class Conversations::ActivityMessageJob < ApplicationJob
|
||||||
queue_as :default
|
queue_as :default
|
||||||
|
|
||||||
def perform(conversation, message_params)
|
def perform(conversation, message_params)
|
||||||
conversation.messages.create(message_params)
|
conversation.messages.create!(message_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ class Conversations::UserMentionJob < ApplicationJob
|
||||||
)
|
)
|
||||||
|
|
||||||
if mention.nil?
|
if mention.nil?
|
||||||
Mention.create(
|
Mention.create!(
|
||||||
user_id: mentioned_user_id,
|
user_id: mentioned_user_id,
|
||||||
conversation_id: conversation_id,
|
conversation_id: conversation_id,
|
||||||
mentioned_at: Time.zone.now,
|
mentioned_at: Time.zone.now,
|
||||||
|
|
|
@ -16,7 +16,7 @@ class ReportingEventListener < BaseListener
|
||||||
event_start_time: conversation.created_at,
|
event_start_time: conversation.created_at,
|
||||||
event_end_time: conversation.updated_at
|
event_end_time: conversation.updated_at
|
||||||
)
|
)
|
||||||
reporting_event.save
|
reporting_event.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def first_reply_created(event)
|
def first_reply_created(event)
|
||||||
|
@ -39,6 +39,6 @@ class ReportingEventListener < BaseListener
|
||||||
# rubocop:disable Rails/SkipsModelValidations
|
# rubocop:disable Rails/SkipsModelValidations
|
||||||
conversation.update_columns(first_reply_created_at: message.created_at)
|
conversation.update_columns(first_reply_created_at: message.created_at)
|
||||||
# rubocop:enable Rails/SkipsModelValidations
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
reporting_event.save
|
reporting_event.save!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ module MailboxHelper
|
||||||
def create_message
|
def create_message
|
||||||
return if @conversation.messages.find_by(source_id: processed_mail.message_id).present?
|
return if @conversation.messages.find_by(source_id: processed_mail.message_id).present?
|
||||||
|
|
||||||
@message = @conversation.messages.create(
|
@message = @conversation.messages.create!(
|
||||||
account_id: @conversation.account_id,
|
account_id: @conversation.account_id,
|
||||||
sender: @conversation.contact,
|
sender: @conversation.contact,
|
||||||
content: mail_content&.truncate(150_000),
|
content: mail_content&.truncate(150_000),
|
||||||
|
|
|
@ -39,7 +39,7 @@ class Channel::FacebookPage < ApplicationRecord
|
||||||
def create_contact_inbox(instagram_id, name)
|
def create_contact_inbox(instagram_id, name)
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
contact = inbox.account.contacts.create!(name: name)
|
contact = inbox.account.contacts.create!(name: name)
|
||||||
::ContactInbox.create(
|
::ContactInbox.create!(
|
||||||
contact_id: contact.id,
|
contact_id: contact.id,
|
||||||
inbox_id: inbox.id,
|
inbox_id: inbox.id,
|
||||||
source_id: instagram_id
|
source_id: instagram_id
|
||||||
|
|
|
@ -30,7 +30,7 @@ class Line::IncomingMessageService
|
||||||
def message_created?(event)
|
def message_created?(event)
|
||||||
return unless event_type_message?(event)
|
return unless event_type_message?(event)
|
||||||
|
|
||||||
@message = @conversation.messages.create(
|
@message = @conversation.messages.create!(
|
||||||
content: event['message']['text'],
|
content: event['message']['text'],
|
||||||
account_id: @inbox.account_id,
|
account_id: @inbox.account_id,
|
||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
|
|
|
@ -6,7 +6,7 @@ class Sms::IncomingMessageService
|
||||||
def perform
|
def perform
|
||||||
set_contact
|
set_contact
|
||||||
set_conversation
|
set_conversation
|
||||||
@message = @conversation.messages.create(
|
@message = @conversation.messages.create!(
|
||||||
content: params[:text],
|
content: params[:text],
|
||||||
account_id: @inbox.account_id,
|
account_id: @inbox.account_id,
|
||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
|
|
|
@ -12,7 +12,7 @@ class Telegram::IncomingMessageService
|
||||||
set_contact
|
set_contact
|
||||||
update_contact_avatar
|
update_contact_avatar
|
||||||
set_conversation
|
set_conversation
|
||||||
@message = @conversation.messages.create(
|
@message = @conversation.messages.create!(
|
||||||
content: params[:message][:text].presence || params[:message][:caption],
|
content: params[:message][:text].presence || params[:message][:caption],
|
||||||
account_id: @inbox.account_id,
|
account_id: @inbox.account_id,
|
||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
|
|
|
@ -8,7 +8,7 @@ class Twilio::IncomingMessageService
|
||||||
|
|
||||||
set_contact
|
set_contact
|
||||||
set_conversation
|
set_conversation
|
||||||
@message = @conversation.messages.create(
|
@message = @conversation.messages.create!(
|
||||||
content: params[:Body],
|
content: params[:Body],
|
||||||
account_id: @inbox.account_id,
|
account_id: @inbox.account_id,
|
||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
|
|
|
@ -7,7 +7,7 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService
|
||||||
set_inbox
|
set_inbox
|
||||||
ensure_contacts
|
ensure_contacts
|
||||||
set_conversation
|
set_conversation
|
||||||
@message = @conversation.messages.create(
|
@message = @conversation.messages.create!(
|
||||||
content: message_create_data['message_data']['text'],
|
content: message_create_data['message_data']['text'],
|
||||||
account_id: @inbox.account_id,
|
account_id: @inbox.account_id,
|
||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
|
@ -30,7 +30,7 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService
|
||||||
def save_media_urls(file)
|
def save_media_urls(file)
|
||||||
@message.content_attributes[:media_url] = file['media_url']
|
@message.content_attributes[:media_url] = file['media_url']
|
||||||
@message.content_attributes[:display_url] = file['display_url']
|
@message.content_attributes[:display_url] = file['display_url']
|
||||||
@message.save
|
@message.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def direct_message_events_params
|
def direct_message_events_params
|
||||||
|
@ -121,6 +121,6 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService
|
||||||
content_type: media['type']
|
content_type: media['type']
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@message.save
|
@message.save!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -80,7 +80,7 @@ class Twitter::TweetParserService < Twitter::WebhooksBaseService
|
||||||
def create_message
|
def create_message
|
||||||
find_or_create_contact(user)
|
find_or_create_contact(user)
|
||||||
set_conversation
|
set_conversation
|
||||||
@conversation.messages.create(
|
@conversation.messages.create!(
|
||||||
account_id: @inbox.account_id,
|
account_id: @inbox.account_id,
|
||||||
sender: @contact,
|
sender: @contact,
|
||||||
content: tweet_text,
|
content: tweet_text,
|
||||||
|
|
42
app/views/layouts/portal.html.erb
Normal file
42
app/views/layouts/portal.html.erb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<%#
|
||||||
|
# Application Layout
|
||||||
|
|
||||||
|
This view template is used as the layout
|
||||||
|
for every page that Administrate generates.
|
||||||
|
|
||||||
|
By default, it renders:
|
||||||
|
- Navigation
|
||||||
|
- Content for a search bar
|
||||||
|
(if provided by a `content_for` block in a nested page)
|
||||||
|
- Flashes
|
||||||
|
- Links to stylesheets and JavaScripts
|
||||||
|
%>
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= I18n.locale %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="ROBOTS" content="NOODP">
|
||||||
|
<meta name="viewport" content="initial-scale=1">
|
||||||
|
|
||||||
|
<%= javascript_pack_tag 'portal' %>
|
||||||
|
<%= stylesheet_pack_tag 'portal' %>
|
||||||
|
|
||||||
|
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
|
<title><%= @portal.page_title%></title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="app-container">
|
||||||
|
<main class="main-content min-h-screen flex flex-col" role="main">
|
||||||
|
<%= render "public/api/v1/portals/header", portal: @portal %>
|
||||||
|
<%= yield %>
|
||||||
|
<%= render "public/api/v1/portals/footer" %>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
44
app/views/public/api/v1/portals/_category-block.html.erb
Normal file
44
app/views/public/api/v1/portals/_category-block.html.erb
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<section class="bg-white lg:container w-full py-6 px-4 flex flex-col h-full">
|
||||||
|
<div class="flex justify-between items-center w-full">
|
||||||
|
<h3 class="text-xl text-slate-900 font-semibold subpixel-antialiased leading-relaxed hover:underline"">
|
||||||
|
<a href="/hc/<%= portal.slug %>/<%= category.locale %>/<%= category.slug %>"><%= category.name %> </a>
|
||||||
|
</h3> <span class="text-slate-500"><%= category.articles.published.size %> articles</span>
|
||||||
|
</div>
|
||||||
|
<div class="py-4 w-full mt-2 flex-grow">
|
||||||
|
<% if category.articles.published.size == 0 %>
|
||||||
|
<div class="h-full flex items-center justify-center bg-slate-50 rounded-xl mb-4">
|
||||||
|
<p class="text-sm text-slate-500">No articles here</p>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<% category.articles.published.take(5).each do |article| %>
|
||||||
|
<div class="flex justify-between content-center h-8 my-1">
|
||||||
|
<a class="text-slate-800 hover:underline leading-8"
|
||||||
|
href="/hc/<%= portal.slug %>/<%= category.locale %>/<%= category.slug %>/<%= article.id %>" class=""><%= article.title %></a>
|
||||||
|
<span class="flex items-center">
|
||||||
|
<svg class="w-4 h-4 fill-current text-slate-700" width="24" height="24" fill="none" viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M8.47 4.22a.75.75 0 0 0 0 1.06L15.19 12l-6.72 6.72a.75.75 0 1 0 1.06 1.06l7.25-7.25a.75.75 0 0 0 0-1.06L9.53 4.22a.75.75 0 0 0-1.06 0Z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/hc/<%= portal.slug %>/<%= category.locale %>/<%= category.slug %>"
|
||||||
|
class="flex flex-row items-center text-base font-sans font-medium text-woot-600 hover:text-slate-900 hover:underline mt-4">
|
||||||
|
|
||||||
|
View all articles
|
||||||
|
<span class="ml-2">
|
||||||
|
<svg class="w-4 h-4 fill-current text-woot-500" width="24" height="24" fill="none" viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
6
app/views/public/api/v1/portals/_footer.html.erb
Normal file
6
app/views/public/api/v1/portals/_footer.html.erb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<footer class="bg-slate-50 py-16 flex flex-col items-center justify-center">
|
||||||
|
<div class="mx-auto max-w-2xl">
|
||||||
|
<p class="text-slate-700 py-2 text-center">Made with <a href="/" target="_blank">Chatwoot 💙</a>.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
37
app/views/public/api/v1/portals/_header.html.erb
Normal file
37
app/views/public/api/v1/portals/_header.html.erb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<header class="bg-white mx-auto px-4 max-w-4xl w-full border border-slate-600">
|
||||||
|
<nav class="px-0 flex" aria-label="Top">
|
||||||
|
<div class="w-full py-4 flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<a href="#">
|
||||||
|
<span class="sr-only"><%= portal.name %>%></span>
|
||||||
|
<img class="h-8 w-auto"
|
||||||
|
src="https://d33wubrfki0l68.cloudfront.net/973467c532160fd8b940300a43fa85fa2d060307/dc9a0/static/brand-73f58cdefae282ae74cebfa74c1d7003.svg"
|
||||||
|
alt="">
|
||||||
|
</a>
|
||||||
|
<div class="ml-8 border-l-1 border-slate-50">
|
||||||
|
<div class="flex-grow flex-shrink-0">
|
||||||
|
<a href="#" class="flex flex-row items-center text-sm font-sans font-medium text-slate-700 hover:text-slate-800 hover:underline"> Goto main site
|
||||||
|
<span class="ml-2">
|
||||||
|
<svg class="w-4 h-4 fill-current text-slate-600" width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z" /></svg>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-grow justify-end flex-shrink-0 items-center">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
value="<%= @portal.config["default_locale"] %>"
|
||||||
|
data-portal-slug="<%= @portal.slug %>"
|
||||||
|
class="h-8 block w-full flex-shrink bg-slate-50 border border-slate-200 text-slate-700 py-1 px-4 pr-8 rounded leading-tight text-base font-medium focus:outline-none focus:bg-white focus:border-slate-500 locale-switcher">
|
||||||
|
<% @portal.config["allowed_locales"].each do |locale| %>
|
||||||
|
<option value="<%= locale %>"><%= locale %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
7
app/views/public/api/v1/portals/_hero.html.erb
Normal file
7
app/views/public/api/v1/portals/_hero.html.erb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<section class="bg-slate-50 py-24 flex flex-col items-center justify-center">
|
||||||
|
<div class="mx-auto max-w-2xl">
|
||||||
|
<h1 class="text-4xl text-slate-900 font-semibold subpixel-antialiased leading-relaxed text-center"><%= portal.header_text %></h1>
|
||||||
|
<p class="text-slate-700 py-2 text-center">Search for the articles here or browse the categories below.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
35
app/views/public/api/v1/portals/articles/index.html.erb
Normal file
35
app/views/public/api/v1/portals/articles/index.html.erb
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
|
||||||
|
<div class="bg-slate-50">
|
||||||
|
<div class="max-w-4xl px-6 py-16 mx-auto space-y-12 w-full">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<a class="text-slate-800 hover:underline leading-8"
|
||||||
|
href="/hc/<%= @portal.slug %>/<%= @category.slug %>" class=""><%= @portal.name %> Home</a>
|
||||||
|
<span>/</span>
|
||||||
|
<span>/</span>
|
||||||
|
</div>
|
||||||
|
<% @articles.each do |article| %>
|
||||||
|
<h1 class="text-4xl font-bold md:tracking-normal leading-snug md:text-5xl text-slate-900">
|
||||||
|
<%= article.title %></h1>
|
||||||
|
<div class="flex flex-col items-start justify-between w-full md:flex-row md:items-center pt-2">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<img src="<%= article.author.avatar_url %>" alt="" class="w-12 h-812 border rounded-full">
|
||||||
|
<div>
|
||||||
|
<h5 class="text-base font-medium text-slate-900 mb-2"><%= article.author.name %></h5>
|
||||||
|
<p class="text-sm font-normal text-slate-700">
|
||||||
|
<%= article.author.updated_at.strftime("%B %d %Y") %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="max-w-4xl flex-grow w-full px-6 py-16 mx-auto space-y-12">
|
||||||
|
<article class="space-y-8 ">
|
||||||
|
<div class="text-slate-800 font-sans leading-8 text-lg subpixel-antialiased max-w-3xl blog-content">
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
35
app/views/public/api/v1/portals/articles/show.html.erb
Normal file
35
app/views/public/api/v1/portals/articles/show.html.erb
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<div class="bg-slate-50">
|
||||||
|
<div class="max-w-4xl px-6 py-16 mx-auto space-y-4 w-full">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a class="text-slate-700 hover:underline leading-8 text-sm font-medium"
|
||||||
|
href="/hc/<%= @portal.slug %>/<%= @article.category.locale %>" class=""><%= @portal.name %> Home</a>
|
||||||
|
<span class="text-xs text-slate-600 px-1">/</span>
|
||||||
|
<a class="text-slate-700 hover:underline leading-8 text-sm font-medium"
|
||||||
|
href="/hc/<%= @portal.slug %>/<%= @article.category.locale %>/<%= @article.category.slug %>"
|
||||||
|
class=""><%= @article.category.name %></a>
|
||||||
|
<span class="text-xs text-slate-600 px-1">/</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-4xl font-bold md:tracking-normal leading-snug md:text-5xl text-slate-900">
|
||||||
|
<%= @article.title %></h1>
|
||||||
|
<div class="flex flex-col items-start justify-between w-full md:flex-row md:items-center pt-2">
|
||||||
|
<div class="flex items-center md:space-x-2">
|
||||||
|
<img src="<%= @article.author.avatar_url %>" alt="" class="w-12 h-812 border rounded-full">
|
||||||
|
<div>
|
||||||
|
<h5 class="text-base font-medium text-slate-900 mb-2"><%= @article.author.name %></h5>
|
||||||
|
<p class="text-sm font-normal text-slate-700">
|
||||||
|
<%= @article.author.updated_at.strftime("%B %d %Y") %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="max-w-4xl flex-grow w-full px-6 py-16 mx-auto space-y-12">
|
||||||
|
<article class="space-y-8 ">
|
||||||
|
<div class="text-slate-800 font-sans leading-8 text-lg subpixel-antialiased max-w-3xl blog-content">
|
||||||
|
<p><%= @parsed_content %></p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<section class="bg-white lg:container w-full py-6 px-4 flex flex-col h-full">
|
||||||
|
<div class="flex justify-between items-center w-full">
|
||||||
|
<h3 class="text-xl text-slate-900 font-semibold subpixel-antialiased leading-relaxed hover:underline"">
|
||||||
|
<a href="/hc/<%= portal.slug %>/<%= category.locale %>/<%= category.slug %>"><%= category.name %> </a>
|
||||||
|
</h3> <span class="text-slate-500"><%= category.articles.published.size %> articles</span>
|
||||||
|
</div>
|
||||||
|
<div class="py-4 w-full mt-2 flex-grow">
|
||||||
|
<% if category.articles.published.size == 0 %>
|
||||||
|
<div class="h-full flex items-center justify-center bg-slate-50 rounded-xl mb-4">
|
||||||
|
<p class="text-sm text-slate-500">No articles here</p>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<% category.articles.published.take(5).each do |article| %>
|
||||||
|
<div class="flex justify-between content-center h-8 my-1">
|
||||||
|
<a class="text-slate-800 hover:underline leading-8"
|
||||||
|
href="/hc/<%= portal.slug %>/<%= category.locale %>/<%= category.slug %>/<%= article.id %>" class=""><%= article.title %></a>
|
||||||
|
<span class="flex items-center">
|
||||||
|
<svg class="w-4 h-4 fill-current text-slate-700" width="24" height="24" fill="none" viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M8.47 4.22a.75.75 0 0 0 0 1.06L15.19 12l-6.72 6.72a.75.75 0 1 0 1.06 1.06l7.25-7.25a.75.75 0 0 0 0-1.06L9.53 4.22a.75.75 0 0 0-1.06 0Z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/hc/<%= portal.slug %>/<%= category.locale %>/<%= category.slug %>"
|
||||||
|
class="flex flex-row items-center text-base font-sans font-medium text-woot-600 hover:text-slate-900 hover:underline mt-4">
|
||||||
|
|
||||||
|
View all articles
|
||||||
|
<span class="ml-2">
|
||||||
|
<svg class="w-4 h-4 fill-current text-woot-500" width="24" height="24" fill="none" viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<!-- This example requires Tailwind CSS v2.0+ -->
|
||||||
|
<section class="bg-slate-50 py-24 flex flex-col items-center justify-center">
|
||||||
|
<div class="mx-auto max-w-2xl">
|
||||||
|
<h1 class="text-4xl text-slate-900 font-semibold subpixel-antialiased leading-relaxed text-center"><%= portal.header_text %></h1>
|
||||||
|
<p class="text-slate-700 py-2 text-center">Search for the articles here or browse the categories below.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
11
app/views/public/api/v1/portals/categories/index.html.erb
Normal file
11
app/views/public/api/v1/portals/categories/index.html.erb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<%= render "hero", portal: @portal %>
|
||||||
|
|
||||||
|
<div class="max-w-4xl w-full flex-grow mx-auto py-16">
|
||||||
|
<div class="grid grid-cols-2 gap-x-32 gap-y-12">
|
||||||
|
<% @categories.each do |category| %>
|
||||||
|
<%= render "category-block", category: category, portal: @portal %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
41
app/views/public/api/v1/portals/categories/show.html.erb
Normal file
41
app/views/public/api/v1/portals/categories/show.html.erb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
|
||||||
|
<div class="bg-slate-50">
|
||||||
|
<div class="max-w-4xl px-6 py-16 mx-auto space-y-8">
|
||||||
|
<a class="text-slate-700 text-sm hover:underline leading-8"
|
||||||
|
href="/hc/<%= @portal.slug %>/<%= @category.locale %>" class=""><%= @portal.name %> Home</a>
|
||||||
|
<span class="text-xs text-slate-600 px-1">/</span>
|
||||||
|
|
||||||
|
<div class="flex justify-start items-center w-full">
|
||||||
|
<h1 class="text-3xl font-bold md:tracking-normal leading-snug text-slate-900">
|
||||||
|
<%= @category.name %></h1>
|
||||||
|
<span class="text-slate-500 px-8"><%= @category.articles.published.size %> articles</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<section class="bg-white max-w-4xl w-full mx-auto py-6 px-4 flex flex-col items-center justify-center flex-grow">
|
||||||
|
|
||||||
|
<div class="py-4 w-full mt-2 flex-grow">
|
||||||
|
<% if @category.articles.published.size == 0 %>
|
||||||
|
<div class="h-full flex items-center justify-center bg-slate-50 rounded-xl">
|
||||||
|
<p class="text-sm text-slate-500">No articles here</p>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<% @category.articles.published.each do |article| %>
|
||||||
|
<div class="flex justify-between content-center h-8 my-1">
|
||||||
|
<a class="text-slate-800 hover:underline"
|
||||||
|
href="/hc/<%= @portal.slug %>/<%= @category.locale %>/<%= @category.slug %>/<%= article.id %>"
|
||||||
|
class=""><%= article.title %></a>
|
||||||
|
<span>
|
||||||
|
<svg class="w-4 h-4 fill-current text-slate-700" width="24" height="24" fill="none" viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M8.47 4.22a.75.75 0 0 0 0 1.06L15.19 12l-6.72 6.72a.75.75 0 1 0 1.06 1.06l7.25-7.25a.75.75 0 0 0 0-1.06L9.53 4.22a.75.75 0 0 0-1.06 0Z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
12
app/views/public/api/v1/portals/show.html.erb
Normal file
12
app/views/public/api/v1/portals/show.html.erb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
<%= render "hero", portal: @portal %>
|
||||||
|
|
||||||
|
<div class="max-w-4xl w-full flex-grow mx-auto py-16">
|
||||||
|
<div class="grid grid-cols-2 gap-x-32 gap-y-12">
|
||||||
|
<% @portal.categories.each do |category| %>
|
||||||
|
<%= render "category-block", category: category, portal: @portal %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -287,11 +287,11 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get 'hc/:slug/:locale', to: 'public/api/v1/portals#show', format: 'json'
|
get 'hc/:slug/:locale', to: 'public/api/v1/portals#show'
|
||||||
get 'hc/:slug/:locale/categories', to: 'public/api/v1/portals/categories#index', format: 'json'
|
get 'hc/:slug/:locale/categories', to: 'public/api/v1/portals/categories#index'
|
||||||
get 'hc/:slug/:locale/:category_slug', to: 'public/api/v1/portals/categories#show', format: 'json'
|
get 'hc/:slug/:locale/:category_slug', to: 'public/api/v1/portals/categories#show'
|
||||||
get 'hc/:slug/:locale/:category_slug/articles', to: 'public/api/v1/portals/articles#index', format: 'json'
|
get 'hc/:slug/:locale/:category_slug/articles', to: 'public/api/v1/portals/articles#index'
|
||||||
get 'hc/:slug/:locale/:category_slug/:id', to: 'public/api/v1/portals/articles#show', format: 'json'
|
get 'hc/:slug/:locale/:category_slug/:id', to: 'public/api/v1/portals/articles#show'
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# Used in mailer templates
|
# Used in mailer templates
|
||||||
|
|
|
@ -7,7 +7,7 @@ class ChatwootHub
|
||||||
|
|
||||||
def self.installation_identifier
|
def self.installation_identifier
|
||||||
identifier = InstallationConfig.find_by(name: 'INSTALLATION_IDENTIFIER')&.value
|
identifier = InstallationConfig.find_by(name: 'INSTALLATION_IDENTIFIER')&.value
|
||||||
identifier ||= InstallationConfig.create(name: 'INSTALLATION_IDENTIFIER', value: SecureRandom.uuid).value
|
identifier ||= InstallationConfig.create!(name: 'INSTALLATION_IDENTIFIER', value: SecureRandom.uuid).value
|
||||||
identifier
|
identifier
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -84,7 +84,7 @@ class Integrations::Csml::ProcessorService < Integrations::BotProcessorService
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_text_messages(message_payload, conversation)
|
def process_text_messages(message_payload, conversation)
|
||||||
conversation.messages.create(
|
conversation.messages.create!(
|
||||||
{
|
{
|
||||||
message_type: :outgoing,
|
message_type: :outgoing,
|
||||||
account_id: conversation.account_id,
|
account_id: conversation.account_id,
|
||||||
|
@ -99,7 +99,7 @@ class Integrations::Csml::ProcessorService < Integrations::BotProcessorService
|
||||||
buttons = message_payload['content']['buttons'].map do |button|
|
buttons = message_payload['content']['buttons'].map do |button|
|
||||||
{ title: button['content']['title'], value: button['content']['payload'] }
|
{ title: button['content']['title'], value: button['content']['payload'] }
|
||||||
end
|
end
|
||||||
conversation.messages.create(
|
conversation.messages.create!(
|
||||||
{
|
{
|
||||||
message_type: :outgoing,
|
message_type: :outgoing,
|
||||||
account_id: conversation.account_id,
|
account_id: conversation.account_id,
|
||||||
|
|
|
@ -43,10 +43,10 @@ class Integrations::Dialogflow::ProcessorService < Integrations::BotProcessorSer
|
||||||
return if content_params.blank?
|
return if content_params.blank?
|
||||||
|
|
||||||
conversation = message.conversation
|
conversation = message.conversation
|
||||||
conversation.messages.create(content_params.merge({
|
conversation.messages.create!(content_params.merge({
|
||||||
message_type: :outgoing,
|
message_type: :outgoing,
|
||||||
account_id: conversation.account_id,
|
account_id: conversation.account_id,
|
||||||
inbox_id: conversation.inbox_id
|
inbox_id: conversation.inbox_id
|
||||||
}))
|
}))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -83,7 +83,7 @@ class Integrations::Slack::IncomingMessageBuilder
|
||||||
def create_message
|
def create_message
|
||||||
return unless conversation
|
return unless conversation
|
||||||
|
|
||||||
@message = conversation.messages.create(
|
@message = conversation.messages.create!(
|
||||||
message_type: :outgoing,
|
message_type: :outgoing,
|
||||||
account_id: conversation.account_id,
|
account_id: conversation.account_id,
|
||||||
inbox_id: conversation.inbox_id,
|
inbox_id: conversation.inbox_id,
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"@chatwoot/utils": "^0.0.10",
|
"@chatwoot/utils": "^0.0.10",
|
||||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||||
"@rails/actioncable": "6.1.3",
|
"@rails/actioncable": "6.1.3",
|
||||||
|
"@rails/ujs": "^7.0.3-1",
|
||||||
"@rails/webpacker": "5.3.0",
|
"@rails/webpacker": "5.3.0",
|
||||||
"@sentry/tracing": "^6.19.7",
|
"@sentry/tracing": "^6.19.7",
|
||||||
"@sentry/vue": "^6.19.7",
|
"@sentry/vue": "^6.19.7",
|
||||||
|
@ -50,6 +51,7 @@
|
||||||
"semver": "7.3.5",
|
"semver": "7.3.5",
|
||||||
"spinkit": "~1.2.5",
|
"spinkit": "~1.2.5",
|
||||||
"tailwindcss": "^1.9.6",
|
"tailwindcss": "^1.9.6",
|
||||||
|
"turbolinks": "^5.2.0",
|
||||||
"url-loader": "^2.0.0",
|
"url-loader": "^2.0.0",
|
||||||
"v-tooltip": "~2.1.3",
|
"v-tooltip": "~2.1.3",
|
||||||
"videojs-record": "^4.5.0",
|
"videojs-record": "^4.5.0",
|
||||||
|
|
|
@ -26,7 +26,7 @@ RSpec.describe '/api/v1/widget/labels', type: :request do
|
||||||
|
|
||||||
context 'with correct website token and a defined label' do
|
context 'with correct website token and a defined label' do
|
||||||
before do
|
before do
|
||||||
account.labels.create(title: 'customer-support')
|
account.labels.create!(title: 'customer-support')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'add the label to the conversation' do
|
it 'add the label to the conversation' do
|
||||||
|
|
|
@ -20,10 +20,6 @@ RSpec.describe 'Public Articles API', type: :request do
|
||||||
get "/hc/#{portal.slug}/#{category.locale}/#{category.slug}/articles"
|
get "/hc/#{portal.slug}/#{category.locale}/#{category.slug}/articles"
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
json_response = JSON.parse(response.body)
|
|
||||||
|
|
||||||
expect(json_response['payload'].length).to eql portal.articles.count
|
|
||||||
expect(json_response['meta']['articles_count']).to be json_response['payload'].size
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'get all articles with searched text query' do
|
it 'get all articles with searched text query' do
|
||||||
|
@ -39,9 +35,6 @@ RSpec.describe 'Public Articles API', type: :request do
|
||||||
headers: agent.create_new_auth_token,
|
headers: agent.create_new_auth_token,
|
||||||
params: { query: 'funny' }
|
params: { query: 'funny' }
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
json_response = JSON.parse(response.body)
|
|
||||||
expect(json_response['payload'].count).to be 1
|
|
||||||
expect(json_response['meta']['articles_count']).to be json_response['payload'].size
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -50,9 +43,6 @@ RSpec.describe 'Public Articles API', type: :request do
|
||||||
get "/hc/#{portal.slug}/#{category.locale}/#{category.slug}/#{article.id}"
|
get "/hc/#{portal.slug}/#{category.locale}/#{category.slug}/#{article.id}"
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
json_response = JSON.parse(response.body)
|
|
||||||
|
|
||||||
expect(json_response['title']).to eql article.title
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,9 +14,6 @@ RSpec.describe 'Public Portals API', type: :request do
|
||||||
get "/hc/#{portal.slug}/en"
|
get "/hc/#{portal.slug}/en"
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
json_response = JSON.parse(response.body)
|
|
||||||
expect(json_response['slug']).to eql 'test-portal'
|
|
||||||
expect(json_response['meta']['articles_count']).to be 0
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'Throws unauthorised error for unknown domain' do
|
it 'Throws unauthorised error for unknown domain' do
|
||||||
|
|
|
@ -43,7 +43,7 @@ describe AutomationRuleListener do
|
||||||
])
|
])
|
||||||
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
||||||
automation_rule.files.attach(file)
|
automation_rule.files.attach(file)
|
||||||
automation_rule.save
|
automation_rule.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#conversation_updated with contacts attributes' do
|
describe '#conversation_updated with contacts attributes' do
|
||||||
|
|
|
@ -19,7 +19,7 @@ RSpec.describe ReplyMailbox, type: :mailbox do
|
||||||
before do
|
before do
|
||||||
# this UUID is hardcoded in the reply.eml, that's why we are updating this
|
# this UUID is hardcoded in the reply.eml, that's why we are updating this
|
||||||
conversation.uuid = '6bdc3f4d-0bec-4515-a284-5d916fdde489'
|
conversation.uuid = '6bdc3f4d-0bec-4515-a284-5d916fdde489'
|
||||||
conversation.save
|
conversation.save!
|
||||||
|
|
||||||
described_subject
|
described_subject
|
||||||
end
|
end
|
||||||
|
@ -129,7 +129,7 @@ RSpec.describe ReplyMailbox, type: :mailbox do
|
||||||
before do
|
before do
|
||||||
# this UUID is hardcoded in the reply.eml, that's why we are updating this
|
# this UUID is hardcoded in the reply.eml, that's why we are updating this
|
||||||
conversation.uuid = '6bdc3f4d-0bec-4515-a284-5d916fdde489'
|
conversation.uuid = '6bdc3f4d-0bec-4515-a284-5d916fdde489'
|
||||||
conversation.save
|
conversation.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'add the mail content as new message on the conversation' do
|
it 'add the mail content as new message on the conversation' do
|
||||||
|
|
|
@ -32,7 +32,7 @@ RSpec.describe SupportMailbox, type: :mailbox do
|
||||||
before do
|
before do
|
||||||
# this email is hardcoded in the support.eml, that's why we are updating this
|
# this email is hardcoded in the support.eml, that's why we are updating this
|
||||||
channel_email.email = 'care@example.com'
|
channel_email.email = 'care@example.com'
|
||||||
channel_email.save
|
channel_email.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'covers email address format' do
|
describe 'covers email address format' do
|
||||||
|
@ -121,7 +121,7 @@ RSpec.describe SupportMailbox, type: :mailbox do
|
||||||
before do
|
before do
|
||||||
# this email is hardcoded eml fixture file that's why we are updating this
|
# this email is hardcoded eml fixture file that's why we are updating this
|
||||||
channel_email.email = 'support@chatwoot.com'
|
channel_email.email = 'support@chatwoot.com'
|
||||||
channel_email.save
|
channel_email.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'create new contact with original sender' do
|
it 'create new contact with original sender' do
|
||||||
|
|
|
@ -54,15 +54,15 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
|
||||||
|
|
||||||
it 'renders the subject in conversation as reply' do
|
it 'renders the subject in conversation as reply' do
|
||||||
conversation.additional_attributes = { 'mail_subject': 'Mail Subject' }
|
conversation.additional_attributes = { 'mail_subject': 'Mail Subject' }
|
||||||
conversation.save
|
conversation.save!
|
||||||
new_message.save
|
new_message.save!
|
||||||
expect(mail.subject).to eq('Re: Mail Subject')
|
expect(mail.subject).to eq('Re: Mail Subject')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'not have private notes' do
|
it 'not have private notes' do
|
||||||
# make the message private
|
# make the message private
|
||||||
private_message.private = true
|
private_message.private = true
|
||||||
private_message.save
|
private_message.save!
|
||||||
|
|
||||||
expect(mail.body.decoded).not_to include(private_message.content)
|
expect(mail.body.decoded).not_to include(private_message.content)
|
||||||
expect(mail.body.decoded).to include(message.content)
|
expect(mail.body.decoded).to include(message.content)
|
||||||
|
@ -104,7 +104,7 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
|
||||||
let(:mail) { described_class.reply_without_summary(message_2.conversation, message_2.id).deliver_now }
|
let(:mail) { described_class.reply_without_summary(message_2.conversation, message_2.id).deliver_now }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
message_2.save
|
message_2.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'renders the default subject' do
|
it 'renders the default subject' do
|
||||||
|
@ -113,14 +113,14 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
|
||||||
|
|
||||||
it 'renders the subject in conversation' do
|
it 'renders the subject in conversation' do
|
||||||
conversation.additional_attributes = { 'mail_subject': 'Mail Subject' }
|
conversation.additional_attributes = { 'mail_subject': 'Mail Subject' }
|
||||||
conversation.save
|
conversation.save!
|
||||||
expect(mail.subject).to eq('Mail Subject')
|
expect(mail.subject).to eq('Mail Subject')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'not have private notes' do
|
it 'not have private notes' do
|
||||||
# make the message private
|
# make the message private
|
||||||
private_message.private = true
|
private_message.private = true
|
||||||
private_message.save
|
private_message.save!
|
||||||
expect(mail.body.decoded).not_to include(private_message.content)
|
expect(mail.body.decoded).not_to include(private_message.content)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ RSpec.describe Campaign, type: :model do
|
||||||
let(:campaign) { build(:campaign, inbox: website_inbox, display_id: nil, trigger_rules: { url: 'https://test.com' }) }
|
let(:campaign) { build(:campaign, inbox: website_inbox, display_id: nil, trigger_rules: { url: 'https://test.com' }) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
campaign.save
|
campaign.save!
|
||||||
campaign.reload
|
campaign.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ RSpec.describe Conversation, type: :model do
|
||||||
let(:conversation) { build(:conversation, display_id: nil) }
|
let(:conversation) { build(:conversation, display_id: nil) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
conversation.save
|
conversation.save!
|
||||||
conversation.reload
|
conversation.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ RSpec.describe ConversationReplyEmailWorker, type: :worker do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'calls ConversationSummaryMailer#reply_without_summary when last incoming message was from email' do
|
it 'calls ConversationSummaryMailer#reply_without_summary when last incoming message was from email' do
|
||||||
message.save
|
message.save!
|
||||||
described_class.new.perform(1, message.id)
|
described_class.new.perform(1, message.id)
|
||||||
expect(mailer).to have_received(:reply_without_summary)
|
expect(mailer).to have_received(:reply_without_summary)
|
||||||
end
|
end
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -1878,6 +1878,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.1.3.tgz#c8a67ec4d22ecd6931f7ebd98143fddbc815419a"
|
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.1.3.tgz#c8a67ec4d22ecd6931f7ebd98143fddbc815419a"
|
||||||
integrity sha512-m02524MR9cTnUNfGz39Lkx9jVvuL0tle4O7YgvouJ7H83FILxzG1nQ5jw8pAjLAr9XQGu+P1sY4SKE3zyhCNjw==
|
integrity sha512-m02524MR9cTnUNfGz39Lkx9jVvuL0tle4O7YgvouJ7H83FILxzG1nQ5jw8pAjLAr9XQGu+P1sY4SKE3zyhCNjw==
|
||||||
|
|
||||||
|
"@rails/ujs@^7.0.3-1":
|
||||||
|
version "7.0.3-1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.0.3-1.tgz#0a0f4f2b22b887bcbf6e0b0a72b8c86665cd31d9"
|
||||||
|
integrity sha512-g3LgpBAsWmW97xFxh5OTDgyEJLt63fEENJUYb/iNFRXY6aKLI/by6MjFw7x492DSP/+vKQa3oMEdNnjI9+yZgQ==
|
||||||
|
|
||||||
"@rails/webpacker@5.3.0":
|
"@rails/webpacker@5.3.0":
|
||||||
version "5.3.0"
|
version "5.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-5.3.0.tgz#9d7a615735f850572b9c5e2ad4c57f4af70d70fd"
|
resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-5.3.0.tgz#9d7a615735f850572b9c5e2ad4c57f4af70d70fd"
|
||||||
|
@ -15087,6 +15092,11 @@ tunnel-agent@^0.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
|
turbolinks@^5.2.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/turbolinks/-/turbolinks-5.2.0.tgz#e6877a55ea5c1cb3bb225f0a4ae303d6d32ff77c"
|
||||||
|
integrity sha512-pMiez3tyBo6uRHFNNZoYMmrES/IaGgMhQQM+VFF36keryjb5ms0XkVpmKHkfW/4Vy96qiGW3K9bz0tF5sK9bBw==
|
||||||
|
|
||||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||||
version "0.14.5"
|
version "0.14.5"
|
||||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||||
|
|
Loading…
Reference in a new issue