Merge branch 'develop' into feat/add_lograge

This commit is contained in:
Vishnu Narayanan 2022-09-15 16:59:45 +05:30 committed by GitHub
commit a4e1730297
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 599 additions and 182 deletions

View file

@ -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

View file

@ -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

View file

@ -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
) )

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
) )

View file

@ -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>

View file

@ -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>

View file

@ -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);
}, },

View file

@ -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;

View file

@ -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',

View file

@ -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"
}, },

View file

@ -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,

View file

@ -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;

View file

@ -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"
> >

View file

@ -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',

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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() {

View file

@ -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) {

View file

@ -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>

View 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);

View 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;
}
}

View 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}/`;
});
};

View 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)
);
});
});

View file

@ -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;
}; };

View file

@ -34,6 +34,7 @@ export const hasEmojiSupport = () => {
}; };
export const removeEmoji = text => { export const removeEmoji = text => {
if (text) {
return text return text
.replace( .replace(
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
@ -41,4 +42,6 @@ export const removeEmoji = text => {
) )
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
.trim(); .trim();
}
return '';
}; };

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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),

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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,

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -43,7 +43,7 @@ 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

View file

@ -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,

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"