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
|
||||
|
||||
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
|
||||
|
||||
def update_identifier_subscription
|
||||
|
|
|
@ -49,7 +49,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
|||
def clone
|
||||
automation_rule = Current.account.automation_rules.find_by(id: params[:automation_rule_id])
|
||||
new_rule = automation_rule.dup
|
||||
new_rule.save
|
||||
new_rule.save!
|
||||
@automation_rule = new_rule
|
||||
end
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
|||
phone_number: phone_number,
|
||||
medium: medium
|
||||
)
|
||||
@inbox = Current.account.inboxes.create(
|
||||
@inbox = Current.account.inboxes.create!(
|
||||
name: permitted_params[:name],
|
||||
channel: @twilio_channel
|
||||
)
|
||||
|
|
|
@ -135,7 +135,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
|
||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
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
|
||||
|
||||
def permitted_params
|
||||
|
|
|
@ -9,7 +9,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||
ActiveRecord::Base.transaction do
|
||||
process_update_contact
|
||||
@conversation = create_conversation
|
||||
conversation.messages.create(message_params)
|
||||
conversation.messages.create!(message_params)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -59,7 +59,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||
|
||||
unless conversation.resolved?
|
||||
conversation.status = :resolved
|
||||
conversation.save
|
||||
conversation.save!
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
|
|
@ -23,7 +23,7 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle
|
|||
|
||||
def authenticate_resource_with_sso_token
|
||||
@token = @resource.create_token
|
||||
@resource.save
|
||||
@resource.save!
|
||||
|
||||
sign_in(:user, @resource, store: false, bypass: false)
|
||||
# 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_category
|
||||
before_action :set_article, only: [:show]
|
||||
layout 'portal'
|
||||
|
||||
def index
|
||||
@articles = @portal.articles
|
||||
|
@ -15,6 +16,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
|
|||
|
||||
def set_article
|
||||
@article = @category.articles.find(params[:id])
|
||||
@parsed_content = render_article_content(@article.content)
|
||||
end
|
||||
|
||||
def set_category
|
||||
|
@ -28,4 +30,10 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
|
|||
def list_params
|
||||
params.permit(:query)
|
||||
end
|
||||
|
||||
def render_article_content(content)
|
||||
# rubocop:disable Rails/OutputSafety
|
||||
CommonMarker.render_html(content).html_safe
|
||||
# rubocop:enable Rails/OutputSafety
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ class Public::Api::V1::Portals::CategoriesController < PublicController
|
|||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||
before_action :set_portal
|
||||
before_action :set_category, only: [:show]
|
||||
layout 'portal'
|
||||
|
||||
def index
|
||||
@categories = @portal.categories
|
||||
|
@ -12,7 +13,7 @@ class Public::Api::V1::Portals::CategoriesController < PublicController
|
|||
private
|
||||
|
||||
def set_category
|
||||
@category = @portal.categories.find_by!(locale: params[:locale])
|
||||
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
|
||||
end
|
||||
|
||||
def set_portal
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
class Public::Api::V1::PortalsController < PublicController
|
||||
before_action :ensure_custom_domain_request, only: [:show]
|
||||
before_action :set_portal
|
||||
layout 'portal'
|
||||
|
||||
def show; end
|
||||
|
||||
|
|
|
@ -44,12 +44,12 @@ class Twitter::CallbacksController < Twitter::BaseController
|
|||
end
|
||||
|
||||
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_secret: parsed_body['oauth_token_secret'],
|
||||
profile_id: parsed_body['user_id']
|
||||
)
|
||||
account.inboxes.create(
|
||||
account.inboxes.create!(
|
||||
name: parsed_body['screen_name'],
|
||||
channel: twitter_profile
|
||||
)
|
||||
|
|
|
@ -285,8 +285,6 @@ export default {
|
|||
}
|
||||
|
||||
.secondary-menu .nested.vertical.menu {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
margin-left: var(--space-small);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -245,6 +245,8 @@ export default {
|
|||
@import '~dashboard/assets/scss/woot';
|
||||
|
||||
.secondary-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--white);
|
||||
border-right: 1px solid var(--s-50);
|
||||
height: 100%;
|
||||
|
@ -267,7 +269,6 @@ export default {
|
|||
.menu {
|
||||
padding: var(--space-small);
|
||||
overflow-y: auto;
|
||||
height: 94%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -37,17 +37,6 @@
|
|||
size="small"
|
||||
/>
|
||||
</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
|
||||
v-if="showAudioRecorderButton"
|
||||
:icon="!isRecordingAudio ? 'microphone' : 'microphone-off'"
|
||||
|
@ -128,10 +117,7 @@
|
|||
<script>
|
||||
import FileUpload from 'vue-upload-component';
|
||||
import * as ActiveStorage from 'activestorage';
|
||||
import {
|
||||
hasPressedAltAndWKey,
|
||||
hasPressedAltAndAKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import { hasPressedAltAndAKey } from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
|
@ -207,10 +193,6 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
setFormatMode: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
isFormatMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
@ -219,10 +201,6 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
enableRichEditor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
enterToSendEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
@ -296,16 +274,10 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (hasPressedAltAndWKey(e)) {
|
||||
this.toggleFormatMode();
|
||||
}
|
||||
if (hasPressedAltAndAKey(e)) {
|
||||
this.$refs.upload.$children[1].$el.click();
|
||||
}
|
||||
},
|
||||
toggleFormatMode() {
|
||||
this.setFormatMode(!this.isFormatMode);
|
||||
},
|
||||
toggleEnterToSend() {
|
||||
this.$emit('toggleEnterToSend', !this.enterToSendEnabled);
|
||||
},
|
||||
|
|
|
@ -107,10 +107,8 @@
|
|||
:recording-audio-duration-text="recordingAudioDurationText"
|
||||
:recording-audio-state="recordingAudioState"
|
||||
:is-recording-audio="isRecordingAudio"
|
||||
:set-format-mode="setFormatMode"
|
||||
:is-on-private-note="isOnPrivateNote"
|
||||
:is-format-mode="showRichContentEditor"
|
||||
:enable-rich-editor="isRichEditorEnabled"
|
||||
:enter-to-send-enabled="enterToSendEnabled"
|
||||
:enable-multiple-file-upload="enableMultipleFileUpload"
|
||||
:has-whatsapp-templates="hasWhatsappTemplates"
|
||||
|
@ -229,17 +227,10 @@ export default {
|
|||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
showRichContentEditor() {
|
||||
if (this.isOnPrivateNote) {
|
||||
if (this.isOnPrivateNote || this.isRichEditorEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.isRichEditorEnabled) {
|
||||
const {
|
||||
display_rich_content_editor: displayRichContentEditor,
|
||||
} = this.uiSettings;
|
||||
|
||||
return displayRichContentEditor;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
assignedAgent: {
|
||||
|
@ -375,7 +366,7 @@ export default {
|
|||
);
|
||||
},
|
||||
isRichEditorEnabled() {
|
||||
return this.isAWebWidgetInbox || this.isAnEmailChannel;
|
||||
return this.isAWebWidgetInbox || this.isAnEmailChannel || this.isAPIInbox;
|
||||
},
|
||||
showAudioRecorder() {
|
||||
return !this.isOnPrivateNote && this.showFileUpload;
|
||||
|
@ -799,9 +790,6 @@ export default {
|
|||
|
||||
return messagePayload;
|
||||
},
|
||||
setFormatMode(value) {
|
||||
this.updateUISettings({ display_rich_content_editor: value });
|
||||
},
|
||||
setCcEmails(value) {
|
||||
this.bccEmails = value.bccEmails;
|
||||
this.ccEmails = value.ccEmails;
|
||||
|
|
|
@ -65,12 +65,6 @@ export const SHORTCUT_KEYS = [
|
|||
firstkey: 'Alt / ⌥',
|
||||
secondKey: 'P',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
label: 'TOGGLE_RICH_CONTENT_EDITOR',
|
||||
firstkey: 'Alt / ⌥',
|
||||
secondKey: 'W',
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
label: 'SWITCH_TO_REPLY',
|
||||
|
|
|
@ -253,7 +253,6 @@
|
|||
"GO_TO_SETTINGS": "Go to Settings",
|
||||
"SWITCH_CONVERSATION_STATUS": "Switch to the next conversation status",
|
||||
"SWITCH_TO_PRIVATE_NOTE": "Switch to Private Note",
|
||||
"TOGGLE_RICH_CONTENT_EDITOR": "Toggle Rich Content editor",
|
||||
"SWITCH_TO_REPLY": "Switch to Reply",
|
||||
"TOGGLE_SNOOZE_DROPDOWN": "Toggle snooze dropdown"
|
||||
},
|
||||
|
|
|
@ -16,7 +16,6 @@ describe('uiSettingsMixin', () => {
|
|||
actions = { updateUISettings: jest.fn(), toggleSidebarUIState: jest.fn() };
|
||||
getters = {
|
||||
getUISettings: () => ({
|
||||
display_rich_content_editor: false,
|
||||
enter_to_send_enabled: false,
|
||||
is_ct_labels_open: true,
|
||||
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||
|
@ -34,7 +33,6 @@ describe('uiSettingsMixin', () => {
|
|||
};
|
||||
const wrapper = shallowMount(Component, { store, localVue });
|
||||
expect(wrapper.vm.uiSettings).toEqual({
|
||||
display_rich_content_editor: false,
|
||||
enter_to_send_enabled: false,
|
||||
is_ct_labels_open: true,
|
||||
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||
|
@ -55,7 +53,6 @@ describe('uiSettingsMixin', () => {
|
|||
expect.anything(),
|
||||
{
|
||||
uiSettings: {
|
||||
display_rich_content_editor: false,
|
||||
enter_to_send_enabled: true,
|
||||
is_ct_labels_open: true,
|
||||
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||
|
@ -80,7 +77,6 @@ describe('uiSettingsMixin', () => {
|
|||
expect.anything(),
|
||||
{
|
||||
uiSettings: {
|
||||
display_rich_content_editor: false,
|
||||
enter_to_send_enabled: false,
|
||||
is_ct_labels_open: false,
|
||||
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<header class="header">
|
||||
<div class="table-actions-wrap">
|
||||
<div class="left-aligned-wrap">
|
||||
<woot-sidemenu-icon />
|
||||
<h1 class="page-title">
|
||||
{{ headerTitle }}
|
||||
</h1>
|
||||
|
@ -173,11 +174,13 @@ export default {
|
|||
}
|
||||
|
||||
.search-wrap {
|
||||
width: 400px;
|
||||
max-width: 400px;
|
||||
min-width: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-right: var(--space-small);
|
||||
margin-left: var(--space-small);
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<woot-button
|
||||
icon="chevron-left"
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="primary"
|
||||
@click="onClickGoBack"
|
||||
>
|
||||
|
|
|
@ -6,17 +6,16 @@
|
|||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||
/>
|
||||
<div v-if="portals.length">
|
||||
<help-center-sidebar
|
||||
v-if="portals.length"
|
||||
:class="sidebarClassName"
|
||||
:header-title="headerTitle"
|
||||
:sub-title="localeName(selectedPortalLocale)"
|
||||
:sub-title="localeName(selectedLocaleInPortal)"
|
||||
:accessible-menu-items="accessibleMenuItems"
|
||||
:additional-secondary-menu-items="additionalSecondaryMenuItems"
|
||||
@open-popover="openPortalPopover"
|
||||
@open-modal="onClickOpenAddCatogoryModal"
|
||||
/>
|
||||
</div>
|
||||
<section class="app-content columns" :class="contentClassName">
|
||||
<router-view />
|
||||
<command-bar />
|
||||
|
@ -33,13 +32,15 @@
|
|||
v-if="showPortalPopover"
|
||||
:portals="portals"
|
||||
:active-portal-slug="selectedPortalSlug"
|
||||
:active-locale="selectedLocaleInPortal"
|
||||
@close-popover="closePortalPopover"
|
||||
/>
|
||||
<add-category
|
||||
v-if="showAddCategoryModal"
|
||||
:show.sync="showAddCategoryModal"
|
||||
:portal-name="selectedPortalName"
|
||||
:locale="selectedPortalLocale"
|
||||
:locale="selectedLocaleInPortal"
|
||||
:portal-slug="selectedPortalSlug"
|
||||
@cancel="onClickCloseAddCategoryModal"
|
||||
/>
|
||||
</section>
|
||||
|
@ -96,6 +97,9 @@ export default {
|
|||
|
||||
return this.$store.getters['portals/allPortals'][0];
|
||||
},
|
||||
selectedLocaleInPortal() {
|
||||
return this.$route.params.locale || this.defaultPortalLocale;
|
||||
},
|
||||
sidebarClassName() {
|
||||
if (this.isOnDesktop) {
|
||||
return '';
|
||||
|
@ -120,7 +124,7 @@ export default {
|
|||
selectedPortalSlug() {
|
||||
return this.selectedPortal ? this.selectedPortal?.slug : '';
|
||||
},
|
||||
selectedPortalLocale() {
|
||||
defaultPortalLocale() {
|
||||
return this.selectedPortal
|
||||
? this.selectedPortal?.meta?.default_locale
|
||||
: '';
|
||||
|
@ -142,7 +146,7 @@ export default {
|
|||
key: 'list_all_locale_articles',
|
||||
count: allArticlesCount,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles`
|
||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedLocaleInPortal}/articles`
|
||||
),
|
||||
toolTip: 'All Articles',
|
||||
toStateName: 'list_all_locale_articles',
|
||||
|
@ -153,7 +157,7 @@ export default {
|
|||
key: 'list_mine_articles',
|
||||
count: mineArticlesCount,
|
||||
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',
|
||||
toStateName: 'list_mine_articles',
|
||||
|
@ -164,7 +168,7 @@ export default {
|
|||
key: 'list_draft_articles',
|
||||
count: draftArticlesCount,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles/draft`
|
||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedLocaleInPortal}/articles/draft`
|
||||
),
|
||||
toolTip: 'Draft',
|
||||
toStateName: 'list_draft_articles',
|
||||
|
@ -175,7 +179,7 @@ export default {
|
|||
key: 'list_archived_articles',
|
||||
count: archivedArticlesCount,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles/archived`
|
||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedLocaleInPortal}/articles/archived`
|
||||
),
|
||||
toolTip: 'Archived',
|
||||
toStateName: 'list_archived_articles',
|
||||
|
|
|
@ -34,18 +34,11 @@
|
|||
:key="portal.id"
|
||||
:portal="portal"
|
||||
:active-portal-slug="activePortalSlug"
|
||||
:active-locale="activeLocale"
|
||||
:active="portal.slug === activePortalSlug"
|
||||
@open-portal-page="onPortalSelect"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
@ -66,6 +59,10 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeLocale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -125,12 +122,5 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
gap: var(--space-small);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -19,13 +19,13 @@
|
|||
/>
|
||||
</header>
|
||||
<div class="portal-locales">
|
||||
<h5 class="locale-title">
|
||||
<h5 class="locale-title sub-block-title">
|
||||
{{ $t('HELP_CENTER.PORTAL.CHOOSE_LOCALE_LABEL') }}
|
||||
</h5>
|
||||
<ul>
|
||||
<li v-for="locale in locales" :key="locale.code">
|
||||
<woot-button
|
||||
:class="
|
||||
:variant="
|
||||
`locale-item ${
|
||||
isLocaleActive(locale.code, activePortalSlug)
|
||||
? 'smooth'
|
||||
|
@ -38,7 +38,7 @@
|
|||
>
|
||||
<div class="locale-content">
|
||||
<div class="meta">
|
||||
<h6 class="text-block-title text-left">
|
||||
<h6 class="text-block-title text-left locale-name">
|
||||
<span>
|
||||
{{ localeName(locale.code) }}
|
||||
</span>
|
||||
|
@ -90,6 +90,10 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeLocale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -129,7 +133,7 @@ export default {
|
|||
},
|
||||
isLocaleActive(code, slug) {
|
||||
const isPortalActive = this.portal.slug === slug;
|
||||
const isLocaleActive = this.portal?.meta?.default_locale === code;
|
||||
const isLocaleActive = this.activeLocale === code;
|
||||
return isPortalActive && isLocaleActive;
|
||||
},
|
||||
isLocaleDefault(code) {
|
||||
|
@ -151,6 +155,7 @@ export default {
|
|||
|
||||
&.active {
|
||||
border: 1px solid var(--w-400);
|
||||
background: var(---25);
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
|
@ -177,15 +182,13 @@ export default {
|
|||
.portal-count {
|
||||
font-size: var(--font-size-mini);
|
||||
margin-bottom: 0;
|
||||
color: var(--s-500);
|
||||
color: var(--s-600);
|
||||
}
|
||||
}
|
||||
|
||||
.portal-locales {
|
||||
.locale-title {
|
||||
color: var(--s-600);
|
||||
font-size: var(--font-size-default);
|
||||
font-weight: var(--font-weight-medium);
|
||||
.locale-name {
|
||||
margin-bottom: var(--space-micro);
|
||||
}
|
||||
|
||||
.locale-content {
|
||||
|
@ -204,6 +207,7 @@ export default {
|
|||
.locale__radio {
|
||||
width: var(--space-large);
|
||||
margin-top: var(--space-tiny);
|
||||
color: var(--g-600);
|
||||
}
|
||||
|
||||
.add-locale-wrap {
|
||||
|
@ -227,7 +231,7 @@ export default {
|
|||
|
||||
.locale-meta {
|
||||
display: flex;
|
||||
color: var(--s-500);
|
||||
color: var(--s-600);
|
||||
font-size: var(--font-size-small);
|
||||
text-align: left;
|
||||
line-height: var(--space-normal);
|
||||
|
|
|
@ -69,7 +69,7 @@ export default {
|
|||
},
|
||||
portalLink() {
|
||||
const slug = this.$route.params.portalSlug;
|
||||
return `/public/api/v1/portals/${slug}`;
|
||||
return `/hc/${slug}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -89,6 +89,8 @@ export default {
|
|||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/woot';
|
||||
.secondary-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--white);
|
||||
border-right: 1px solid var(--s-50);
|
||||
height: 100%;
|
||||
|
@ -108,5 +110,10 @@ export default {
|
|||
&:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.menu {
|
||||
padding: var(--space-small);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -86,7 +86,7 @@ export default {
|
|||
},
|
||||
portalLink() {
|
||||
const slug = this.$route.params.portalSlug;
|
||||
return `/public/api/v1/portals/${slug}`;
|
||||
return `/hc/${slug}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -86,6 +86,10 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
portalSlug: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -105,7 +109,9 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
selectedPortalSlug() {
|
||||
return this.$route.params.portalSlug;
|
||||
return this.$route.params.portalSlug
|
||||
? this.$route.params.portalSlug
|
||||
: this.portalSlug;
|
||||
},
|
||||
nameError() {
|
||||
if (this.$v.name.$error) {
|
||||
|
|
|
@ -140,13 +140,13 @@ export default {
|
|||
width: 100%;
|
||||
background: var(--white);
|
||||
height: 100%;
|
||||
padding: 0 var(--space-medium);
|
||||
padding: 0 0 0 var(--space-normal);
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.locale-container {
|
||||
margin-top: var(--space-large);
|
||||
margin-top: var(--space-normal);
|
||||
}
|
||||
}
|
||||
</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;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndWKey = e => {
|
||||
return e.altKey && e.keyCode === 87;
|
||||
};
|
||||
|
||||
export const hasPressedAltAndAKey = e => {
|
||||
return e.altKey && e.keyCode === 65;
|
||||
};
|
||||
|
|
|
@ -34,6 +34,7 @@ export const hasEmojiSupport = () => {
|
|||
};
|
||||
|
||||
export const removeEmoji = text => {
|
||||
if (text) {
|
||||
return text
|
||||
.replace(
|
||||
/([\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, ' ')
|
||||
.trim();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
|
|
@ -2,6 +2,6 @@ class Conversations::ActivityMessageJob < ApplicationJob
|
|||
queue_as :default
|
||||
|
||||
def perform(conversation, message_params)
|
||||
conversation.messages.create(message_params)
|
||||
conversation.messages.create!(message_params)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ class Conversations::UserMentionJob < ApplicationJob
|
|||
)
|
||||
|
||||
if mention.nil?
|
||||
Mention.create(
|
||||
Mention.create!(
|
||||
user_id: mentioned_user_id,
|
||||
conversation_id: conversation_id,
|
||||
mentioned_at: Time.zone.now,
|
||||
|
|
|
@ -16,7 +16,7 @@ class ReportingEventListener < BaseListener
|
|||
event_start_time: conversation.created_at,
|
||||
event_end_time: conversation.updated_at
|
||||
)
|
||||
reporting_event.save
|
||||
reporting_event.save!
|
||||
end
|
||||
|
||||
def first_reply_created(event)
|
||||
|
@ -39,6 +39,6 @@ class ReportingEventListener < BaseListener
|
|||
# rubocop:disable Rails/SkipsModelValidations
|
||||
conversation.update_columns(first_reply_created_at: message.created_at)
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
reporting_event.save
|
||||
reporting_event.save!
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ module MailboxHelper
|
|||
def create_message
|
||||
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,
|
||||
sender: @conversation.contact,
|
||||
content: mail_content&.truncate(150_000),
|
||||
|
|
|
@ -39,7 +39,7 @@ class Channel::FacebookPage < ApplicationRecord
|
|||
def create_contact_inbox(instagram_id, name)
|
||||
ActiveRecord::Base.transaction do
|
||||
contact = inbox.account.contacts.create!(name: name)
|
||||
::ContactInbox.create(
|
||||
::ContactInbox.create!(
|
||||
contact_id: contact.id,
|
||||
inbox_id: inbox.id,
|
||||
source_id: instagram_id
|
||||
|
|
|
@ -30,7 +30,7 @@ class Line::IncomingMessageService
|
|||
def message_created?(event)
|
||||
return unless event_type_message?(event)
|
||||
|
||||
@message = @conversation.messages.create(
|
||||
@message = @conversation.messages.create!(
|
||||
content: event['message']['text'],
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
|
|
|
@ -6,7 +6,7 @@ class Sms::IncomingMessageService
|
|||
def perform
|
||||
set_contact
|
||||
set_conversation
|
||||
@message = @conversation.messages.create(
|
||||
@message = @conversation.messages.create!(
|
||||
content: params[:text],
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
|
|
|
@ -12,7 +12,7 @@ class Telegram::IncomingMessageService
|
|||
set_contact
|
||||
update_contact_avatar
|
||||
set_conversation
|
||||
@message = @conversation.messages.create(
|
||||
@message = @conversation.messages.create!(
|
||||
content: params[:message][:text].presence || params[:message][:caption],
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
|
|
|
@ -8,7 +8,7 @@ class Twilio::IncomingMessageService
|
|||
|
||||
set_contact
|
||||
set_conversation
|
||||
@message = @conversation.messages.create(
|
||||
@message = @conversation.messages.create!(
|
||||
content: params[:Body],
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
|
|
|
@ -7,7 +7,7 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService
|
|||
set_inbox
|
||||
ensure_contacts
|
||||
set_conversation
|
||||
@message = @conversation.messages.create(
|
||||
@message = @conversation.messages.create!(
|
||||
content: message_create_data['message_data']['text'],
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
|
@ -30,7 +30,7 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService
|
|||
def save_media_urls(file)
|
||||
@message.content_attributes[:media_url] = file['media_url']
|
||||
@message.content_attributes[:display_url] = file['display_url']
|
||||
@message.save
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def direct_message_events_params
|
||||
|
@ -121,6 +121,6 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService
|
|||
content_type: media['type']
|
||||
}
|
||||
)
|
||||
@message.save
|
||||
@message.save!
|
||||
end
|
||||
end
|
||||
|
|
|
@ -80,7 +80,7 @@ class Twitter::TweetParserService < Twitter::WebhooksBaseService
|
|||
def create_message
|
||||
find_or_create_contact(user)
|
||||
set_conversation
|
||||
@conversation.messages.create(
|
||||
@conversation.messages.create!(
|
||||
account_id: @inbox.account_id,
|
||||
sender: @contact,
|
||||
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
|
||||
|
||||
get 'hc/:slug/:locale', to: 'public/api/v1/portals#show', format: 'json'
|
||||
get 'hc/:slug/:locale/categories', to: 'public/api/v1/portals/categories#index', format: 'json'
|
||||
get 'hc/:slug/:locale/:category_slug', to: 'public/api/v1/portals/categories#show', format: 'json'
|
||||
get 'hc/:slug/:locale/:category_slug/articles', to: 'public/api/v1/portals/articles#index', format: 'json'
|
||||
get 'hc/:slug/:locale/:category_slug/:id', to: 'public/api/v1/portals/articles#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'
|
||||
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'
|
||||
get 'hc/:slug/:locale/:category_slug/:id', to: 'public/api/v1/portals/articles#show'
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Used in mailer templates
|
||||
|
|
|
@ -7,7 +7,7 @@ class ChatwootHub
|
|||
|
||||
def self.installation_identifier
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ class Integrations::Csml::ProcessorService < Integrations::BotProcessorService
|
|||
end
|
||||
|
||||
def process_text_messages(message_payload, conversation)
|
||||
conversation.messages.create(
|
||||
conversation.messages.create!(
|
||||
{
|
||||
message_type: :outgoing,
|
||||
account_id: conversation.account_id,
|
||||
|
@ -99,7 +99,7 @@ class Integrations::Csml::ProcessorService < Integrations::BotProcessorService
|
|||
buttons = message_payload['content']['buttons'].map do |button|
|
||||
{ title: button['content']['title'], value: button['content']['payload'] }
|
||||
end
|
||||
conversation.messages.create(
|
||||
conversation.messages.create!(
|
||||
{
|
||||
message_type: :outgoing,
|
||||
account_id: conversation.account_id,
|
||||
|
|
|
@ -43,7 +43,7 @@ class Integrations::Dialogflow::ProcessorService < Integrations::BotProcessorSer
|
|||
return if content_params.blank?
|
||||
|
||||
conversation = message.conversation
|
||||
conversation.messages.create(content_params.merge({
|
||||
conversation.messages.create!(content_params.merge({
|
||||
message_type: :outgoing,
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id
|
||||
|
|
|
@ -83,7 +83,7 @@ class Integrations::Slack::IncomingMessageBuilder
|
|||
def create_message
|
||||
return unless conversation
|
||||
|
||||
@message = conversation.messages.create(
|
||||
@message = conversation.messages.create!(
|
||||
message_type: :outgoing,
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"@chatwoot/utils": "^0.0.10",
|
||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||
"@rails/actioncable": "6.1.3",
|
||||
"@rails/ujs": "^7.0.3-1",
|
||||
"@rails/webpacker": "5.3.0",
|
||||
"@sentry/tracing": "^6.19.7",
|
||||
"@sentry/vue": "^6.19.7",
|
||||
|
@ -50,6 +51,7 @@
|
|||
"semver": "7.3.5",
|
||||
"spinkit": "~1.2.5",
|
||||
"tailwindcss": "^1.9.6",
|
||||
"turbolinks": "^5.2.0",
|
||||
"url-loader": "^2.0.0",
|
||||
"v-tooltip": "~2.1.3",
|
||||
"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
|
||||
before do
|
||||
account.labels.create(title: 'customer-support')
|
||||
account.labels.create!(title: 'customer-support')
|
||||
end
|
||||
|
||||
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"
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
params: { query: 'funny' }
|
||||
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
|
||||
|
||||
|
@ -50,9 +43,6 @@ RSpec.describe 'Public Articles API', type: :request do
|
|||
get "/hc/#{portal.slug}/#{category.locale}/#{category.slug}/#{article.id}"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
|
||||
expect(json_response['title']).to eql article.title
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,9 +14,6 @@ RSpec.describe 'Public Portals API', type: :request do
|
|||
get "/hc/#{portal.slug}/en"
|
||||
|
||||
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
|
||||
|
||||
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')
|
||||
automation_rule.files.attach(file)
|
||||
automation_rule.save
|
||||
automation_rule.save!
|
||||
end
|
||||
|
||||
describe '#conversation_updated with contacts attributes' do
|
||||
|
|
|
@ -19,7 +19,7 @@ RSpec.describe ReplyMailbox, type: :mailbox do
|
|||
before do
|
||||
# this UUID is hardcoded in the reply.eml, that's why we are updating this
|
||||
conversation.uuid = '6bdc3f4d-0bec-4515-a284-5d916fdde489'
|
||||
conversation.save
|
||||
conversation.save!
|
||||
|
||||
described_subject
|
||||
end
|
||||
|
@ -129,7 +129,7 @@ RSpec.describe ReplyMailbox, type: :mailbox do
|
|||
before do
|
||||
# this UUID is hardcoded in the reply.eml, that's why we are updating this
|
||||
conversation.uuid = '6bdc3f4d-0bec-4515-a284-5d916fdde489'
|
||||
conversation.save
|
||||
conversation.save!
|
||||
end
|
||||
|
||||
it 'add the mail content as new message on the conversation' do
|
||||
|
|
|
@ -32,7 +32,7 @@ RSpec.describe SupportMailbox, type: :mailbox do
|
|||
before do
|
||||
# this email is hardcoded in the support.eml, that's why we are updating this
|
||||
channel_email.email = 'care@example.com'
|
||||
channel_email.save
|
||||
channel_email.save!
|
||||
end
|
||||
|
||||
describe 'covers email address format' do
|
||||
|
@ -121,7 +121,7 @@ RSpec.describe SupportMailbox, type: :mailbox do
|
|||
before do
|
||||
# this email is hardcoded eml fixture file that's why we are updating this
|
||||
channel_email.email = 'support@chatwoot.com'
|
||||
channel_email.save
|
||||
channel_email.save!
|
||||
end
|
||||
|
||||
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
|
||||
conversation.additional_attributes = { 'mail_subject': 'Mail Subject' }
|
||||
conversation.save
|
||||
new_message.save
|
||||
conversation.save!
|
||||
new_message.save!
|
||||
expect(mail.subject).to eq('Re: Mail Subject')
|
||||
end
|
||||
|
||||
it 'not have private notes' do
|
||||
# make the message private
|
||||
private_message.private = true
|
||||
private_message.save
|
||||
private_message.save!
|
||||
|
||||
expect(mail.body.decoded).not_to include(private_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 }
|
||||
|
||||
before do
|
||||
message_2.save
|
||||
message_2.save!
|
||||
end
|
||||
|
||||
it 'renders the default subject' do
|
||||
|
@ -113,14 +113,14 @@ RSpec.describe ConversationReplyMailer, type: :mailer do
|
|||
|
||||
it 'renders the subject in conversation' do
|
||||
conversation.additional_attributes = { 'mail_subject': 'Mail Subject' }
|
||||
conversation.save
|
||||
conversation.save!
|
||||
expect(mail.subject).to eq('Mail Subject')
|
||||
end
|
||||
|
||||
it 'not have private notes' do
|
||||
# make the message private
|
||||
private_message.private = true
|
||||
private_message.save
|
||||
private_message.save!
|
||||
expect(mail.body.decoded).not_to include(private_message.content)
|
||||
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' }) }
|
||||
|
||||
before do
|
||||
campaign.save
|
||||
campaign.save!
|
||||
campaign.reload
|
||||
end
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ RSpec.describe Conversation, type: :model do
|
|||
let(:conversation) { build(:conversation, display_id: nil) }
|
||||
|
||||
before do
|
||||
conversation.save
|
||||
conversation.save!
|
||||
conversation.reload
|
||||
end
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ RSpec.describe ConversationReplyEmailWorker, type: :worker do
|
|||
end
|
||||
|
||||
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)
|
||||
expect(mailer).to have_received(:reply_without_summary)
|
||||
end
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -1878,6 +1878,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.1.3.tgz#c8a67ec4d22ecd6931f7ebd98143fddbc815419a"
|
||||
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":
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-5.3.0.tgz#9d7a615735f850572b9c5e2ad4c57f4af70d70fd"
|
||||
|
@ -15087,6 +15092,11 @@ tunnel-agent@^0.6.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "0.14.5"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
|
|
Loading…
Reference in a new issue