Upgrade your knowledge base theme
Last Update: Oct 2024 • Est. Read Time: 58 MINIf you created your knowledge base using a theme that was released prior to the Midtown 2.0 version, you may need to manually update your theme files to get you up to date with the latest features.
Who can access this feature? | |
User types | Content administrators can access the Themes page. |
To make these changes, select Edit for the theme, go to More Optionsand select Go to Code Editor.
Select Create New Draft to begin making the necessary edits. Once you're done updating your themes, select Save and Publish.
The following table shows the theme versions that require manual updates to get those features.
Theme version | Feature introduced |
1.0.6 | Article feedback survey |
Capture search article click results | |
Lang parameters to hrefs for proper redirects of article and category pages | |
Portal for end customers and internal users |
Please follow the procedures listed for each version to get yourself up to date.
Tip: To find out what theme version you are on, please go to Settings > Knowledge Base > Themes and hover over the colored label. If you are on a version that requires these updates, you will see the base version you are on.
Upgrade to version 1.0.6
If your theme is on version 1.0.6, please follow the procedures for Version 1.06, 1.0.17, 1.0.18, and 1.0.20 to get you up to date.
- Open the the form.css file and copy and paste the following class under the TextArea section:
.form-field-textarea-charCount { display: flex; justify-content: flex-end; width: 100%; margin-left: 10px; margin-top: 5px; color: #9A9A9A;} .form-field-textarea-charCountOver { color: red; }
- Open the survey.css file and add the following class at the bottom:
/* ============================== Article Survey ============================== */ .article-survey-form { width: 50%; } .article-survey-wrapper { margin: 50px auto; } .article-survey-rating-wrapper, .article-survey-error-wrapper, .article-survey-success-wrapper { display: flex; align-items: center; } .article-survey-rating-button { margin-left: 10px; font-weight: bold; width: 80px; } .article-survey-error-wrapper { margin: 15px 0; color: #D1261E; line-height: 20px; } .article-survey-error-message { margin-left: 5px; } .article-survey-success-icon { color: #74cc98; font-size: 22px; margin-right: 10px; } .article-survey-suggested-reasons-wrapper, .article-survey-writtenFeedback { margin-left: -10px; } .article-survey-suggested-reasons-wrapper > label, .article-survey-writtenFeedback label { font-weight: bold; margin-bottom: 15px; } .article-survey-suggested-reason .form-field-checkbox-input:checked + label::after { top: 0 } .article-survey-suggested-reason label { line-height: 25px; } .article-survey-writtenFeedback textarea { resize: none; width: 100%; } @media only screen and (max-width: 767px) { .article-survey-form { width: 100%; } .article-survey-rating-wrapper { flex-direction: column; align-items: flex-start; } .article-survey-rating-question { margin-bottom: 10px; } .article-survey-rating-button[name='positiveButton'] { margin-left: 0; } }
- Open the article.jsx file and copy and paste the following line directly above the
/article
closing tag.<ArticleSurveyForm articleId={article.articleId} lang={article.lang} orgName={org.name} settings={_.get(org, 'settings.articleSurvey')} />
Feedback surveys will now appear in your articles.
Upgrade to version 1.0.17
If your theme is on version 1.0.17, please follow the procedures for Version 1.0.17, 1.0.18, and 1.0.20 to get you up to date.
- Open the SearchItem.jsx file and replace its contents with the following code.
<React.Fragment> {(() => { class SearchItem extends React.Component { constructor(props) { super(props); this.handleClickSearchResult = this.handleClickSearchResult.bind(this); } handleClickSearchResult(e) { e.preventDefault(); const { org, domain, searchBucketId, searchId, lang, articleId, href } = this.props; if (searchBucketId && searchId) { const url = `https://${org.name}.api.${domain}/p/v1/kb/article-search-buckets/${searchBucketId}/searches/${searchId}`; const articleSearchData = { lang, articleId, }; return fetch(url, { method: "PATCH", body: JSON.stringify(articleSearchData), headers: { "Content-type": "application/json", }, }) .then(() => { window.location.href = href; }) .catch((err) => { console.error(err); window.location.href = href; }); } else { window.location.href = href; } } render() { return ( <div className="article-item-search"> <a href="#" onClick={this.handleClickSearchResult} className="article-item-header-wrap" > <h2 className="article-item-header bold">{title}</h2> </a> <div className="article-item-time"> {publishedAt && plugins.moment(publishedAt).format("MMMM D YYYY h:mma")} </div> <div className="article-item-body"> <div dangerouslySetInnerHTML={{ __html: (body || "") .replace(/<(?:.|\n)*?>/gm, "") .slice(0, 160) .concat("..."), }} /> </div> </div> ); } } SearchItem.defaultProps = { href: "", title: "", publishedAt: "", body: "", org: {}, domain: "", searchBucketId: "", searchId: "", lang: "", articleId: "", }; return React.createElement(SearchItem, { href, title, publishedAt, body, org, domain, searchBucketId, searchId, lang, articleId, }); })()} </React.Fragment>;
- Open the search.jsx file and replace the
SearchItem
with the following code.<SearchItem key={`${el.title}-${el.publishedAt}`} {...el} publishedAt={el.publishedAt} org={org} domain={domain} />
- Open the tag.jsx file and replace the
SearchItem
with the following code.<SearchItem key={el.title} {...el} publishedAt={el.publishedAt} org={org} domain={domain} searchBucketId={undefined} searchId={undefined} />
Now, your Knowledge Base report will show the amount of clicks an article receives after a user performs a search.
Upgrade to version 1.0.18
If your theme is on version 1.0.18, please follow the procedures for Version 1.0.18, and 1.0.20 to get you up to date.
- Open the styles.css file, find .category-wrap-left-aligned::after { and change width: 31% to width: 33%.
- Open the ArticleItem.jsx file, find <a href={/${slug}-${hash}} className="article-item-header-wrap"> on line 2 and replace that code with:
<a href={`/${lang}/${slug}-${hash}`} className="article-item-header-wrap">
- Open the CategoryBlock.jsx file, find <a href={`/categories/${slug}-${hash}`} className="category-block-item"> on line 1 and replace that code with:
<a href={`/${lang}/categories/${slug}-${hash}`} className="category-block-item">
- Open the SubcategoryItem.jsx file, find href={`/categories/${slug}-${hash}`} on line 2 and replace that code with:
href={`/${lang}/categories/${slug}-${hash}`}
- Open the ArticleBreadcrumbs.jsx file, find the two references to href={`/categories/${bc.slug}-${bc.hash}`} and replace both of them with:
href={`/${bc.lang}/categories/${bc.slug}-${bc.hash}`}
Upgrade to version 1.0.20
If your theme is on version 1.0.20, please follow this procedure to get you up to date.
- Create the following new files using the instructions for each one as described below:
- portal.css
- conversation.jsx
- conversations.jsx
- login.jsx
- ConversationBreadcrumbs.jsx
- ConversationDetails.jsx
- ConversationStatus.jsx
- 401.jsx
- Once you are done making all of the changes, Save and Publish your theme.
Note: File names are case sensitive. Please be sure to name each file using the exact letter case shown below.
Create new portal.css file
- Select Add and use the following options to create the new file:
- Theme Target: Global
- File Name: portal
- File Type: CSS
- Copy and paste the following code in the new file:
/* ============================== Conversations ============================== */ .container-header-conversations, .container-header-conversation { text-align: left !important; max-width: 100% !important; } .container-header-conversation { display: flex; align-items: center; } .nav-search-conversations { padding: 0 !important; } .form-control-search-wrap-conversations { width: 100% !important; } .conversations-icon-close { position: absolute; right: 1rem; top: 12px; font-size: 1.5rem; color: #8b97a1; cursor: pointer; } .container-status-filter { max-width: 100%; display: flex; margin: 25px 0; } @media only screen and (min-width: 768px) { .container-status-filter { max-width: 300px; } } .conversations-container { width: 100%; padding: 1.5rem 1rem; } @media only screen and (max-width: 768px) { .conversations-container .container-home { margin: 0 auto; padding: 0 10px; } } .conversations-container .container { margin: 0 auto; } .conversations-breadcrumb { display: flex; flex-wrap: wrap; margin-bottom: 30px; } .conversations-breadcrumbs-link { margin-bottom: 5px; } .breadcrumb-chevron-right-icon { margin: 0 5px; color: #9a9a9a; } .status-filter { cursor: pointer; padding: 10px; border: 1px solid #e3e3e3; display: flex; align-items: center; justify-content: center; width: 100%; text-align: center; color: unset !important; } .status-filter:first-of-type { border-right: none; border-radius: 80px 0px 0px 80px; } .status-filter:last-of-type { border-left: none; border-radius: 0px 80px 80px 0px; } .status-filter-active { background-color: #e3e3e3; } .conversations-list, .conversations-list-item { list-style: none; } .conversations-list { margin: 0; padding: 0; } .conversations-list-item { position: relative; padding: 15px; } .conversations-list-item:nth-child(even) { background-color: rgba(244, 243, 248, 0.4); border-radius: 12px; } .conversations-list-item-link-wrapper { font-weight: bold; display: flex; align-items: center; justify-content: space-between; } .conversations-list-item-left, .conversations-list-item-right { display: flex; align-items: center; } .conversations-list-item-date { color: #9a9a9a; font-size: 12px; } .conversations-list-item-status { border-radius: 80px; padding: 5px 10px; font-weight: bold; font-size: 12px; margin: 0 10px; } .conversations-list-item-status.open { color: #15cc70; background-color: rgba(21, 204, 112, 0.1); } .conversations-list-item-status.done { color: #767676; background-color: rgba(118, 118, 118, 0.1); } .channel-icon-container { font-size: 12px; background-color: #fff; border: 1px solid #e3e3e3; border-radius: 50%; padding: 5px; position: relative; margin-right: 10px; display: flex; align-items: center; justify-content: center; } .conversations-empty { display: flex; align-items: center; flex-direction: column; justify-content: center; text-align: center; padding: 20px 0 40px; background: rgba(244, 243, 248, 0.4); border-radius: 12px; } .conversations-empty-text { color: #767676; } /* ============================== Conversation Details ============================== */ .thread-container { width: 100%; } .thread-container-content { margin: 20px; } .thread-divider { margin: 20px 0; height: 1px; background-color: #e3e3e3; border: none; } .conversation-detail { display: flex; justify-content: space-between; width: 100%; } .details-container { width: 320px; border-left: 1px solid #e3e3e3; padding-left: 20px; } .details-header { display: flex; align-items: center; justify-content: space-between; background-color: #fff; width: 100%; padding: 0; pointer-events: none; } .details-title { font-weight: bold; font-size: 18px; font-family: "Helvetica"; font-family: var(--headingsAndButtonsFont); color: #0a3355; color: var(--headingsColor); } .detail-expand-icon { display: none; } .attributes-container { display: flex; flex-direction: column; } .attribute-container { display: flex; flex-direction: column; padding: 10px 5px; } .attribute-label { color: #767676; font-weight: bold; margin-bottom: 5px; } @media only screen and (max-width: 768px) { .thread-container-content { margin: 0; } .details-container { width: 100%; margin: 20px auto; border-left: none; padding-left: 0; cursor: pointer; } .details-header { background-color: #fafafa; border-radius: 12px; padding: 10px; pointer-events: unset; } .conversation-detail { display: block; } .details-title { font-size: 12px; } .detail-expand-icon { display: block; } .attribute-container { padding: 10px; } .attributes-container.close { display: none; } .attributes-container.open { display: flex; flex-direction: column; } } /* ============================== Conversation Message ============================== */ .messages-container { overflow: auto; height: 500px; margin-bottom: 20px; -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ } .messages-container::-webkit-scrollbar { display: none; } .message-container { display: flex; margin: 0 20px 20px; overflow: visible; } .message-container.out { flex-direction: row; } .message-container.in { flex-direction: row-reverse; } .message-text { font-size: 16px; font-size: var(--baseSizeFont); word-break: break-word; } .message.out { margin-left: 10px; } .message.in { margin-right: 10px; } .message-bubble { max-width: 500px; width: 100%; padding: 10px; font-size: 14px; line-height: 18px; } .message-bubble img { max-width: 100%; max-height: 100%; object-fit: cover; overflow: hidden; } .message-bubble.out { background-color: #fff1f0; border-radius: 0px 10px 10px 10px; } .message-bubble.in { background-color: #f4f3f8; border-radius: 10px 0 10px 10px; } .message-avatar { height: 40px; width: 40px; min-width: 40px; border-radius: 50%; } .message-avatar.out { background-color: #fff1f0; } .message-avatar.in { background-color: #f4f3f8; } .message-timestamp { margin-right: 5px; } .message-meta-container, .message-icon-container { display: flex; position: relative; } .message-icon { color: #15cc70; } .message-icon.pending { color: #767676; } .message-icon.failed { color: #ff655d; } .message-meta { margin-top: 10px; font-size: 14px; color: #767676; display: flex; align-items: baseline; } .message-meta.in { justify-content: flex-end; } .message-meta.out { justify-content: flex-start; } .message-channel-wrapper { display: flex; align-items: baseline; font-weight: bold; } .message-channel { margin-left: 5px; text-transform: capitalize; } .messages-see-more-container { display: flex; justify-content: center; margin: 0 auto 20px; } .messages-see-more-button { padding: 10px; cursor: pointer; } @media only screen and (max-width: 768px) { .message-container { margin: 0 0 20px 0; } } .conversation-message-attachments-container { margin-top: 10px; } /* ============================== Conversation Reply ============================== */ .conversation-reply-form { width: 100%; border: 1px solid #e3e3e3; border-radius: 12px; padding: 10px; } .conversation-reply-textarea { margin-bottom: 0; } .conversation-reply-textarea textarea { width: 100%; border: none; resize: vertical; min-height: 80px; margin: 0; padding: 0; line-height: unset; } .conversation-reply-charLimit { margin-left: 0; } .conversation-reply-textarea label { display: none; } .conversation-actions-container { display: flex; align-items: center; width: 100%; justify-content: flex-end; margin-top: 10px; } .conversation-action-send-button { display: flex; align-items: center; } .conversation-action-send-text { margin-left: 10px; } /* ============================== Conversation Editor ============================== */ .conversation-editor-container { min-height: 200px; } .conversation-editor-attachments-container { border-left: 1px solid #e3e3e3; border-right: 1px solid #e3e3e3; padding: 15px 15px 0; } .conversation-editor-quill-container { padding-bottom: 40px; height: 160px; } #portal-editor { border-bottom: none; border-left: 1px solid #e3e3e3; border-right: 1px solid #e3e3e3; } .conversation-editor-actions-container { display: flex; justify-content: flex-end; align-items: center; padding-bottom: 10px; padding-top: 10px; border-left: 1px solid #e3e3e3; border-right: 1px solid #e3e3e3; border-bottom: 1px solid #e3e3e3; } .conversation-action-attachment { margin-right: 24px; } .conversation-action-attachment-label { cursor: pointer; } .conversation-action-attachment-label-disabled { cursor: not-allowed; } .conversation-action-send-button { margin-right: 10px; background-color: #52aaff; } .conversation-editor-attachments-container .conversation-attachments-container-end-user { justify-content: flex-start; } /* ============================== Conversation Attachments (plural) ============================== */ .conversation-attachments-container-end-user { display: flex; justify-content: flex-end; align-items: center; min-height: 90px; } .conversation-attachments-container-agent { display: flex; justify-content: flex-start; align-items: center; min-height: 90px; } @media only screen and (max-width: 768px) { .conversation-attachments-container-end-user { flex-direction: column; } .conversation-attachments-container-agent { flex-direction: column; } } /* ============================== Conversation Attachment (singular) ============================== */ @media only screen and (max-width: 768px) { .conversation-attachment-container { margin-bottom: 10px; } } .conversation-reply-form-file-attachment { width: 140px; overflow: hidden; display: flex; flex-direction: column; align-items: center; margin-right: 10px; background: #fafafa; border: 1px solid #e3e3e3; border-radius: 12px; padding: 10px; cursor: pointer; background-size: cover; } .conversation-attachment-modal-file { width: 400px; height: 300px; overflow: hidden; display: flex; flex-direction: column; justify-content: center; background: #fafafa; padding: 10px; cursor: pointer; } .conversation-reply-form-file-attachment-delete { align-self: flex-end; } .conversation-reply-form-file-attachment-delete-icon { color: #767676; cursor: pointer; } .conversation-reply-form-file-attachment-wrapper-modal { width: 100%; display: flex; justify-content: center; } .conversation-reply-form-file-attachment-wrapper { width: 100%; display: flex; } .conversation-reply-form-file-attachment-type { background-color: #ff655d; border-radius: 6px; flex: 0 0 auto; font-size: 10px; font-weight: 700; color: white; padding: 5px; } .conversation-reply-form-file-attachment-name-wrapper { display: flex; flex-direction: column; justify-content: center; } .conversation-reply-form-file-attachment-name-modal { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-left: 4px; width: 100%; } .conversation-reply-form-file-attachment-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-left: 4px; width: 80px; } .conversation-reply-form-file-attachment-size-wrapper-modal { display: flex; justify-content: center; } .conversation-reply-form-file-attachment-size-wrapper { align-self: flex-start; } .conversation-reply-form-file-attachment-size-modal { display: inline-block; align-self: flex-start; font-size: 10px; margin-top: 4px; } .conversation-reply-form-file-attachment-size { display: inline-block; align-self: flex-start; font-size: 16px; margin-top: 4px; } /* ============================== Navigation ============================== */ .nav-account-container { position: relative; } .nav-account { display: flex; align-items: center; padding-left: 20px; margin-left: 10px; border-left: 1px solid #efeef6; cursor: pointer; } .nav-account-name { margin: 0 10px; } .nav-account-link { cursor: pointer; } .nav-avatar { height: 20px; width: 20px; border-radius: 50%; border: 1px solid #e3e3e3; } .nav-account-list-container { position: absolute; display: flex; flex-direction: column; background-color: #fff; padding: 20px; width: 200px; right: 0; top: calc(100% + 1rem); box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08); border-radius: 0px 0px 12px 12px; } .nav-account-list-item { list-style: none; margin-bottom: 20px; } .nav-menu-icon { font-size: 22px; } @media only screen and (max-width: 768px) { .nav-avatar { margin-right: 10px; } .nav-account-container.desktop { display: none; } .nav-account-list-container { border: none; border-radius: unset; z-index: 1; position: absolute; width: 100vw; height: 100vh; right: -18px; margin-top: -0.5rem; box-shadow: 0 2px 6px 0 rgb(167 167 167 / 50%); padding: 0; } .nav-account-list-item { margin-bottom: 0; } .nav-account-name { display: none; } } /* ============================== Login ============================== */ .login-buttons-container { display: flex; flex-direction: column; align-items: center; padding: 15px; } .login-buttons-form { padding: 20px; border: 2px solid #ececec; border-radius: 16px; } .login-header { margin-bottom: 20px; } .login-button { border: 1px solid #e3e3e3; border-radius: 12px; display: flex; align-items: center; width: 300px; justify-content: center; padding: 15px; margin-bottom: 15px; } .login-button-text { margin-left: 10px; } /* ============================== Utils ============================== */ .showInMobile { display: none; } .hideInMobile { display: block; } @media only screen and (max-width: 768px) { .showInMobile { display: block; } .hideInMobile { display: none; } } /* ============================== Modal (shared, generic) ============================== */ .shared-modal { margin: auto; padding: 0; border: 0; } .shared-modal-content-conversation-attachment { display: flex; } .shared-modal-title-bar { display: flex; justify-content: space-evenly; position: absolute; right: 0; padding-top: 10px; padding-right: 10px; } .shared-modal-action-icon { font-size: 24px; color: #767676; cursor: pointer; }
Create new conversation.jsx file
- Select Add and use the following options to create the new file:
- Theme Target: Page
- Page Type: Conversation
- Copy and paste the following code in the new file:
<React.Fragment> {(() => { class Conversation extends React.PureComponent { constructor(props) { super(props); this.state = { expandMenu: false, messages: [], links: {}, }; this.handleToggleMenu = this.handleToggleMenu.bind(this); this.scrollToBottom = this.scrollToBottom.bind(this); this.renderConversationDetails = this.renderConversationDetails.bind(this); this.handleClickShowMore = this.handleClickShowMore.bind(this); this.handleClickSend = this.handleClickSend.bind(this); } componentDidMount() { const { data } = this.props; const conversation = _.get(data, "conversation"); const conversationMessages = _.get(conversation, "messages.data"); const conversationMessagesLinks = _.get(conversation, "messages.links"); this.setState({ messages: conversationMessages, links: conversationMessagesLinks }); window.addEventListener("load", () => { this.scrollToBottom(); }); } scrollToBottom() { this.messagesEnd.scrollIntoView({ behavior: "smooth", block: "end" }); } handleToggleMenu() { this.setState((prevState) => ({ expandMenu: !prevState.expandMenu, })); } handleClickSend(msg) { this.setState((prevState) => ({ messages: prevState.messages.concat(msg), }), () => { setTimeout(() => { this.scrollToBottom(); }, 300); }); } handleClickShowMore({ prevMessages, links }) { this.setState((prevState) => ({ messages: prevMessages.concat(prevState.messages), links })); } renderMoreButton() { const { links } = this.state; const { org } = this.props; const conversationMessagesLinkNext = _.get(links, "next"); if (!conversationMessagesLinkNext) return null; return ( <div className="messages-see-more-container"> <ConversationShowMore onClick={this.handleClickShowMore} nextLink={conversationMessagesLinkNext} orgName={org.name} > <Snippet id="sn.kustomer.themebuilder.show_more_text"/>... </ConversationShowMore> </div> ); } renderConversationDetails(className) { const { expandMenu } = this.state; const { data } = this.props; const conversationPage = _.get(data, "org.manifest.pages.conversation"); const conversationVariables = _.get(conversationPage, "variables"); const conversation = _.get(data, "conversation"); return ( <ConversationDetails conversation={conversation} expandMenu={expandMenu} onExpand={this.handleToggleMenu} className={className} data={conversationVariables} /> ); } renderHeader() { const { data } = this.props; const conversationsPage = _.get(data, "org.manifest.pages.conversations"); const conversationsVariables = _.get(conversationsPage, "variables"); const conversation = _.get(data, "conversation"); const conversationName = _.get(conversation, "name"); const conversationStatus = _.get(conversation, "status"); return ( <div className="container-header container-header-conversation"> <h2 className="header-primaryMessage conversations-primaryText"> {conversationName} </h2> <ConversationStatus status={conversationStatus} data={conversationsVariables} /> </div> ); } renderConversation() { const { org, data } = this.props; const { messages } = this.state; const conversationPage = _.get(data, "org.manifest.pages.conversation"); const conversationVariables = _.get(conversationPage, "variables"); const sendButtonText = _.get(conversationVariables, "sendButtonText.value", "Send"); const messageFailedText = _.get(conversationVariables, "messageFailedText.value"); const messagePendingText = _.get(conversationVariables, "messagePendingText.value"); const replyBoxPlaceholderText = _.get(conversationVariables, "replyBoxPlaceholderText.value"); const conversation = _.get(data, "conversation"); const customer = _.get(org, "settings.customer"); const snippetsText = { sendButtonText: eval(sendButtonText), messageFailedText: eval(messageFailedText), messagePendingText: eval(messagePendingText), replyBoxPlaceholderText: eval(replyBoxPlaceholderText), }; return ( <div className="thread-container"> {this.renderHeader()} {this.renderConversationDetails("showInMobile")} <div className="thread-container-content"> <div className="messages-container"> {this.renderMoreButton()} <div className="messages"> {messages.map((msg, idx) => ( <ConversationMessage key={`${msg.id}-${idx}`} message={msg} data={conversationVariables} customer={customer} snippetsText={snippetsText} /> ))} </div> <div id="thread-bottom" ref={(el) => (this.messagesEnd = el)} /> </div> <hr className="thread-divider" /> <ConversationReply conversation={conversation} snippetsText={snippetsText} settings={org.settings} onClick={this.handleClickSend} orgName={org.name} /> </div> </div> ); } render() { const { data } = this.props; const conversation = _.get(data, "conversation"); const conversationName = _.get(conversation, "name"); const lang = _.get(data, "org.settings.lang"); return ( <main className="main-layout"> <Announcement data={findSection(data.org.manifest, "announcement")} template={data.template} /> <Nav data={findSection(data.org.manifest, "header")} settings={org.settings} /> <div className="conversation-detail-container"> <div className="conversations-container"> <ConversationBreadcrumbs view="detail" name={conversationName} lang={lang} /> <section className="container conversation-detail"> {this.renderConversation()} {this.renderConversationDetails("hideInMobile")} </section> </div> </div> <ContactUs data={findSection(data.org.manifest, "contactUs")} /> <Footer data={findSection(data.org.manifest, "footer")} /> </main> ); } } Conversation.defaultProps = { data: {}, org: {}, domain: '' }; return React.createElement(Conversation, { data, org, domain }); })()} </React.Fragment>;
Create new conversations.jsx file
- Select Add and use the following options to create the new file:
- Theme Target: Page
- Page Type: Conversations
- Copy and paste the following code in the new file:
<React.Fragment> {(() => { class Conversations extends React.PureComponent { constructor(props) { super(props); this.state = { query: _.get(props, 'data.query.q', '') } this.renderConversationListItem = this.renderConversationListItem.bind(this); this.handleChange = this.handleChange.bind(this); } handleChange(e) { const query = e.target.value; this.setState({ query, }); } renderHeader() { const { data } = this.props; const { query } = this.state; const conversationsPage = _.get(data, "org.manifest.pages.conversations"); const primaryText = _.get(conversationsPage, "variables.primaryText.value"); const secondaryText = _.get(conversationsPage, "variables.secondaryText.value"); const lang = _.get(data, "org.settings.lang"); const pageReset = replaceUrlParam(window.location.search, 'page', '1'); const conversationsHref = `/${lang}/conversations${replaceUrlParam(pageReset, 'q', '')}`; return ( <div className="container-header container-header-conversations"> <h2 className="header-primaryMessage conversations-primaryText"> {eval(primaryText)} </h2> <h3 className="header-secondaryMessage conversations-secondaryText"> {eval(secondaryText)} </h3> <div className="nav-search nav-search-conversations"> <form className="form-control-search-wrap form-control-search-wrap-conversations" action="/conversations?q=" method="GET" > <label htmlFor="searchInput" hidden> <Snippet id="sn.kustomer.search" /> </label> <i className="icon-search mdi mdi-magnify" aria-hidden="true" /> <input className="form-control form-control-search" id="searchInput" name="q" placeholder={snippet("sn.kustomer.themebuilder.search_bar_label")} aria-label={snippet("sn.kustomer.themebuilder.search_bar_label")} value={query} onChange={this.handleChange} /> {query && ( <a href={conversationsHref}> <i className="conversations-icon-close mdi mdi-close" aria-hidden="true" /> </a> )} </form> </div> </div> ); } renderStatusFilters() { const { data } = this.props; const conversationsPage = _.get(data, "org.manifest.pages.conversations"); const statusFilterBackgroundColor = _.get(conversationsPage, "variables.statusFilterBackgroundColor.value"); const statusFilterSelectedBackgroundColor = _.get(conversationsPage, "variables.statusFilterSelectedBackgroundColor.value"); const conversationStatus = _.get(data, 'query.conversationStatus', undefined); const allSelected = !conversationStatus; const openSelected = conversationStatus === "open"; const doneSelected = conversationStatus === "done"; const lang = _.get(data, "org.settings.lang"); const pageReset = replaceUrlParam(window.location.search, 'page', '1'); return ( <div className="container-status-filter"> <a href={`/${lang}/conversations${replaceUrlParam(pageReset, 'conversationStatus', '')}`} className={`status-filter ${ allSelected ? "status-filter-active conversations-statusFilterSelectedBackgroundColor" : "conversations-statusFilterBackgroundColor" }`} style={{ backgroundColor: allSelected ? statusFilterSelectedBackgroundColor : statusFilterBackgroundColor, }} > <Snippet id="sn.kustomer.themebuilder.conversations_all_text" /> </a> <a href={`/${lang}/conversations${replaceUrlParam(pageReset, 'conversationStatus', 'open')}`} className={`status-filter ${ openSelected ? "status-filter-active conversations-statusFilterSelectedBackgroundColor" : "conversations-statusFilterBackgroundColor" }`} style={{ backgroundColor: openSelected ? statusFilterSelectedBackgroundColor : statusFilterBackgroundColor, }} > <Snippet id="sn.kustomer.themebuilder.conversations_open_status_text" /> </a> <a href={`/${lang}/conversations${replaceUrlParam(pageReset, 'conversationStatus', 'done')}`} className={`status-filter ${ doneSelected ? "status-filter-active conversations-statusFilterSelectedBackgroundColor" : "conversations-statusFilterBackgroundColor" }`} style={{ backgroundColor: doneSelected ? statusFilterSelectedBackgroundColor : statusFilterBackgroundColor, }} > <Snippet id="sn.kustomer.themebuilder.conversations_done_status_text" /> </a> </div> ); } renderConversationListItem(conversation) { const { data } = this.props; const conversationsPage = _.get(data,"org.manifest.pages.conversations"); const conversationsVariables = _.get(conversationsPage, "variables"); const lang = _.get(data, "org.settings.lang"); return ( <li key={conversation.id} className="conversations-list-item"> <a href={`/${lang}/conversations/${conversation.id}`} className="conversations-list-item-link-wrapper" > <div className="conversations-list-item-left"> {conversation.name} </div> <div className="conversations-list-item-right"> <div className="conversations-list-item-date"> {data.plugins .moment(conversation.lastActivityAt) .format("M/D/YYYY")} </div> <ConversationStatus status={conversation.status} data={conversationsVariables} /> </div> </a> </li> ); } renderConversations() { const { data } = this.props; const conversations = _.get(data, "conversations.data", []); return ( <ul className="conversations-list"> {conversations.map(this.renderConversationListItem)} </ul> ); } renderPagination() { const { data } = this.props; const conversations = _.get(data, "conversations.data", []); if (!conversations.length) return null; const links = _.get(data, "conversations.links"); const currentPage = _.get(data, "conversations.meta.page"); const lang = _.get(data, "org.settings.lang"); return ( <SimplePagination currentPage={currentPage} links={links} resource={`${lang}/conversations`} /> ); } render() { const { data } = this.props; const lang = _.get(data, "org.settings.lang"); return ( <main className="main-layout"> <Announcement data={findSection(data.org.manifest, "announcement")} template={data.template} /> <Nav data={findSection(data.org.manifest, "header")} settings={org.settings} /> <div className="conversations-container"> <ConversationBreadcrumbs view="list" lang={lang} /> <section className="container-home conversations"> {this.renderHeader()} {this.renderStatusFilters()} {this.renderConversations()} {this.renderPagination()} </section> </div> <ContactUs data={findSection(data.org.manifest, "contactUs")} /> <Footer data={findSection(data.org.manifest, "footer")} /> </main> ); } } Conversations.defaultProps = { data: {}, org: {}, }; return React.createElement(Conversations, { data, org, }); })()} </React.Fragment>;
Create new login.jsx file
- Select Add and use the following options to create the new file:
- Theme Target: Page
- Page Type: Login
- Copy and paste the following code in the new file:
<React.Fragment> {(() => { class Login extends React.PureComponent { constructor(props) { super(props); } render() { const { data } = this.props; return ( <main className="main-layout"> <Announcement data={findSection(data.org.manifest, "announcement")} template={data.template} /> <Nav data={findSection(data.org.manifest, "header")} settings={org.settings} /> <section className="container-home"> <div className="login-buttons-container"> <div className="login-buttons-form"> <h1 className="login-header"> <Snippet id="sn.kustomer.themebuilder.login_welcome_text" /> </h1> <LoginForm data={data} /> </div> </div> </section> <ContactUs data={findSection(data.org.manifest, "contactUs")} /> <Footer data={findSection(data.org.manifest, "footer")} /> </main> ); } } Login.defaultProps = { data: {}, org: {}, }; return React.createElement(Login, { data, org, }); })()} </React.Fragment>;
Create new ConversationBreadcrumbs.jsx file
- Select Add and use the following options to create the new file:
- Theme Target: Component
- File Name: ConversationBreadcrumbs
- Copy and paste the following code in the new file:
<div className="container conversations-breadcrumb-container"> <div className="conversations-breadcrumb"> <a href={`/lang/${lang}${replaceUrlParam(window.location.search, 'page', '')}`} className="conversations-breadcrumbs-link"> <Snippet id="sn.kustomer.themebuilder.conversations_company_support_text" /> </a> <i className="mdi mdi-chevron-right breadcrumb-chevron-right-icon" aria-hidden="true" /> {view === "list" ? ( <span className="conversations-breadcrumb-text"> <Snippet id="sn.kustomer.themebuilder.conversations_primary_text_value" /> </span> ) : ( <a href={`/${lang}/conversations${window.location.search}`} className="conversations-breadcrumbs-link"> <Snippet id="sn.kustomer.themebuilder.conversations_primary_text_value" /> </a> )} {view === "detail" && name && ( <React.Fragment> <i className="mdi mdi-chevron-right breadcrumb-chevron-right-icon" aria-hidden="true" /> <span className="conversations-breadcrumb-text">{name}</span> </React.Fragment> )} </div> </div>;
Create new ConversationDetails.jsx file
- Select Add and use the following options to create the new file:
- Theme Target: Component
- File Name: ConversationDetails
- Copy and paste the following code in the new file:
<React.Fragment> {(() => { class ConversationDetails extends React.PureComponent { renderAttribute(attr, index) { const attributeType = _.get(attr, 'attributeType'); const attribute = _.get(attr, 'attribute'); const displayName = _.get(attr, 'displayName'); let value = _.get(attr, 'value'); if (Array.isArray(value)) { value = value.join(', '); } else if (attributeType === 'date') { value = plugins.moment(value).format('L LT'); } else if (attributeType === 'url' && value.startsWith('http')) { value = <a href={value} target="_blank" rel="noopener noreferrer">{value}</a>; } return ( <div key={`${attribute}-${index}`} className="attribute-container"> <span className="attribute-label">{displayName}</span> <span className="attribute-value">{value}</span> </div> ); } render() { const { className, onExpand, expandMenu, data, conversation } = this.props; const detailsText = _.get(data, "detailsText.value"); const status = _.get(conversation, 'attributes.status'); const STANDARD_ATTRS = [ { attribute: 'status', attributeType: 'string', type: 'conversation', displayName: <Snippet id="sn.kustomer.themebuilder.status" />, value: status === 'done' ? 'done' : 'open', }, { attribute: "channels", attributeType: 'enum', type: "conversation", displayName: <Snippet id="sn.kustomer.themebuilder.channel" />, value: _.get(conversation, 'attributes.channels'), }, { attribute: "createdAt", attributeType: 'date', type: "conversation", displayName: <Snippet id="sn.kustomer.themebuilder.created_at" />, value: _.get(conversation, 'attributes.createdAt'), }, ]; const allAttrs = STANDARD_ATTRS.concat(_.get(conversation, 'mappedAttributes') || []); return ( <div className={`details-container ${className}`} onClick={onExpand}> <div className="details-header"> <h2 className="header-primaryMessage details-title conversation-detailsText"> {eval(detailsText)} </h2> <i className={`mdi mdi-chevron-${ expandMenu ? "up" : "down" } detail-expand-icon`} aria-hidden="true" /> </div> <div className={`attributes-container ${ expandMenu ? "open" : "close" }`} > {allAttrs.map((attr, index) => { return this.renderAttribute(attr, index); })} </div> </div> ); } } ConversationDetails.defaultProps = { conversation: {}, expandMenu: false, onExpand: () => {}, className: "", data: {}, }; return React.createElement(ConversationDetails, { conversation, expandMenu, onExpand, className, data, }); })()} </React.Fragment>;
Create new ConversationStatus.jsx file
- Select Add and use the following options to create the new file:
- Theme Target: Component
- File Name: ConversationStatus
- Copy and paste the following code in the new file:
<div className={`conversations-list-item-status conversations-${status}StatusBackgroundColor ${status}`} style={{ backgroundColor: status === "done" ? _.get(data, "doneStatusBackgroundColor.value") : _.get(data,"openStatusBackgroundColor.value"), }} > <span className={`conversations-${status}StatusTextColor`} style={{ color: status === "done" ? _.get(data, "doneStatusTextColor.value") : _.get(data, "openStatusTextColor.value"), }} > {status} </span> </div>
Create a new 401.jsx file
- Select Add and use the following options to create the new file:
- Theme Target: Page
- Page Type: 401
- Copy and paste the following code in the new file:
<main className="main-layout"> <Announcement data={findSection(data.org.manifest, "announcement")} template={data.template} /> <Nav data={findSection(data.org.manifest, "header")} settings={org.settings} /> <section className="container-home"> <div> <a href="javascript:history.back()"> <i className="icon-search mdi mdi-arrow-left fourOhFour-icon" aria-hidden="true"></i> </a> </div> <div className="fourOhFour-wrap"> <h1 className="fourOhFour-header"> <Snippet id="sn.kustomer.error" /> 401 </h1> <h2 className="fourOhFour-subheader"> <Snippet id="sn.kustomer.themebuilder.401_message" /> </h2> <div className="fourOhFour-body"> <NotPermittedSvg className="fourOhFour-image" /> <button className="btn" onClick={() => { location.href='/' }}> <Snippet id="sn.kustomer.themebuilder.back_to_home_button" /> </button> </div> </div> </section> <Footer data={findSection(data.org.manifest, "footer")} /> </main>
Step 2: Update existing theme files
Next, update the theme files with the changes shown in this procedure.
Note: If you have existing custom changes that you made to any of the below files, make sure you also port them over.
Update the styles.css file
- Open the styles.css file and make the following changes:
- Find class .container-home and change margin: 2.5rem; to margin: 1.5rem;
- Find class .contactUs-primaryText and add .conversations-primaryText to the list.
- Find class .contactUs-secondaryText and add .conversations-secondaryText to the list.
- Find class .text-overflow and add word-break: break-word;.
- Find .four0hFour and paste the following code near .four0hFour styles:
.fourOhFour-icon { font-size: 1.5rem; } .fourOhFour-image { display: block; margin: 3rem auto; max-width: 100%; }
- Find .article-item-header-wrap and replace that code with:
.article-item-header-wrap { transition: color 0.2s ease-in-out; font-size: 1rem; font-weight: bold; display: flex; }
.article-item-header-wrap:hover .article-item-header-icon:before { color: inherit !important; } .article-item-header-icon { margin-left: 5px; }
- Find class .container-home and change margin: 2.5rem; to margin: 1.5rem;
Update the util.js file
Open the utils.js file and replace the code in it with:
function findSection(manifest, name) { if (Array.isArray(manifest)) { return manifest.filter(function (section) { return section.name === name; })[0]; } else { const section = _.get(manifest, `pages.homepage.variables[${name}]`); return section; } } function getAbsoluteLink(link) { if (!link) return ""; const httpsNotFound = link.indexOf("https://") === -1; const httpNotFound = link.indexOf("http://") === -1; let absoluteLink = link; if (httpsNotFound && httpNotFound) { absoluteLink = `https://${link}`; } return absoluteLink; }
Update the homepage.jsx file
Open the homepage.jsx file and replace the code in it with:
<React.Fragment> {(() => { class Homepage extends React.PureComponent { constructor(props) { super(props); } render() { const manifest = data.org.manifest; return ( <main className="main-layout"> <Announcement key="announcement" data={findSection(manifest, "announcement")} template={data.template} /> <Nav key="header" data={findSection(manifest, "header")} settings={org.settings} /> <SearchHeaderWithSuggestions key="searchBar" data={findSection(manifest, "searchBar")} org={org} domain={data.domain} HeroImage={HeroImage} /> <CategoriesSection key="category" data={findSection(manifest, "category")} categories={categories} CategoryBlock={CategoryBlock} /> <FeaturedArticles key="featuredArticles" data={findSection(manifest, "featuredArticles")} featuredArticles={featuredArticles} ArticleItem={ArticleItem} /> <ContactUs key="contactUs" data={findSection(manifest, "contactUs")} /> <Footer key="footer" data={findSection(manifest, "footer")} /> </main> ); } } return React.createElement(Homepage); })()} </React.Fragment>;
Update the nav.jsx file
Open the nav.jsx file and replace the code in it with:
<React.Fragment> {(() => { class Nav extends React.PureComponent { constructor(props) { super(props); this.mql = window.matchMedia("screen and (min-width: 768px)"); this.state = { showDesktopMenu: this.mql.matches, showMobileMenu: false, lockBody: false, showUserMenu: false, }; this.handleMediaChange = this.handleMediaChange.bind(this); this.handleOpenMobileMenu = this.handleOpenMobileMenu.bind(this); this.handleCloseMobileMenu = this.handleCloseMobileMenu.bind(this); this.renderNavListItem = this.renderNavListItem.bind(this); this.handleLockBody = this.handleLockBody.bind(this); this.handleToggleAccountMenu = this.handleToggleAccountMenu.bind(this); this.handleOnClickUserMenu = this.handleOnClickUserMenu.bind(this); this.handleClickOutside = this.handleClickOutside.bind(this); } componentDidMount() { try { // Chrome & Firefox this.mql.addEventListener( "change", _.throttle(this.handleMediaChange) ); } catch (e) { try { // Safari this.mql.addListener(_.throttle(this.handleMediaChange)); } catch (err) { console.error(err); } } document.addEventListener('mousedown', this.handleClickOutside); } componentWillUnmount() { document.removeEventListener('mousedown', this.handleClickOutside); } handleMediaChange(mediaQueryEvent) { const { showMobileMenu } = this.state; this.setState({ showDesktopMenu: mediaQueryEvent.matches }); if (mediaQueryEvent.matches && showMobileMenu) { this.handleCloseMobileMenu(); } } handleOpenMobileMenu() { document.body.classList.add("lock-body"); this.setState({ showMobileMenu: true }); } handleCloseMobileMenu() { document.body.classList.remove("lock-body"); this.setState({ showMobileMenu: false }); } handleLockBody() { this.setState( (prevState) => ({ lockBody: !prevState.lockBody }), () => { if (this.state.lockBody) { document.body.classList.add("lock-body"); } else { document.body.classList.remove("lock-body"); } } ); } handleToggleAccountMenu() { this.setState((prevState) => { return { showUserMenu: !prevState.showUserMenu, }; }); } handleOnClickUserMenu(mode) { this.handleToggleAccountMenu(); if (mode === "mobile") { this.handleLockBody(); } } handleClickOutside(event) { if (this.userMenu && !this.userMenu.contains(event.target)) { this.setState({ showUserMenu: false }); } } renderAccountList(mode) { const { settings } = this.props; const { showUserMenu } = this.state; if (!showUserMenu) return null; let listItemClassName = "nav-account-list-item"; if (mode === "mobile") { listItemClassName += " nav-list-item nav-list-item-mobile"; } const lang = _.get(settings, 'lang'); const pageReset = replaceUrlParam(window.location.search, 'page', '1'); const conversationsHref = `/${lang}/conversations${replaceUrlParam(pageReset, 'q', '')}`; return ( <ul className="nav-account-list-container" role="menu"> { _.get(settings, "customer") && ( <li className={listItemClassName}> <a href={conversationsHref} className="nav-account-link nav-account-link-conversations"> <Snippet id="sn.kustomer.themebuilder.conversations_primary_text_value" /> </a> </li> ) } <li className={listItemClassName}> <a className="nav-account-link nav-account-link-logout" href={`/logout${window.location.search}`}> <Snippet id="sn.kustomer.themebuilder.logout_text" /> </a> </li> </ul> ); } renderLoginOrUserMenu(mode) { const { settings } = this.props; const { showUserMenu } = this.state; const direction = showUserMenu ? "up" : "down"; const isPortalEnabled = _.get(settings, "portalSettings.enabled", false); const internalModeEnabled = _.get(settings, "portalSettings.internalModeEnabled", false); const customer = _.get(settings, "customer") || _.get(settings, "agent"); if (!isPortalEnabled && !internalModeEnabled) return null; if (!customer && isPortalEnabled) { return ( <div className={`nav-account-container ${mode}`}> <div className="nav-account"> <a href="/login" className="nav-account-link nav-account-link-login"> <Snippet id="sn.kustomer.themebuilder.login_text" /> </a> </div> </div> ); } if (customer) { const avatarUrl = _.get(customer, "avatarUrl"); const name = _.get(customer, "name"); const email = _.get(customer, "email"); return ( <div key={`UserMenu-${mode}`} className={`nav-account-container ${mode}`} onClick={() => this.handleOnClickUserMenu(mode)} ref={(el) => { if (mode === "desktop") { this.userMenu = el; } }} > <div className="nav-account"> {avatarUrl && ( <img src={avatarUrl} className="nav-avatar" alt="avatar-icon" /> )} <div className="nav-account-name">{name || email}</div> <i className={`mdi mdi-menu-${direction} nav-menu-${direction}-icon nav-menu-icon`} aria-hidden="true" /> </div> {this.renderAccountList(mode)} </div> ); } return null; } renderLogo() { const { settings } = this.props; const logo = _.get(settings, "logo"); return ( <div className="nav-flex"> {logo && ( <a href="/" id="top"> <img src={logo} className="header-logo" alt="Homepage" /> </a> )} </div> ); } renderMobileNav() { const { showMobileMenu } = this.state; const iconType = showMobileMenu ? "mdi-close" : "mdi-menu"; const iconOnClick = showMobileMenu ? this.handleCloseMobileMenu : this.handleOpenMobileMenu; return ( <nav className="nav"> <div className="container"> {this.renderLogo()} <div className="nav-flex"> <i className={`mdi ${iconType} nav-mobile-menu-icon`} onClick={iconOnClick} aria-hidden="true" /> {this.renderLoginOrUserMenu("mobile")} </div> </div> {showMobileMenu && this.renderNavList()} </nav> ); } renderNavListItem(item, index) { const { showMobileMenu } = this.state; const navigationLink = _.get(item, "navigationLink.value"); const navigationText = _.get(item, "navigationText.value"); const navListItemClassname = showMobileMenu ? "nav-list-item nav-list-item-mobile" : "nav-list-item"; return ( <li className={navListItemClassname} key={`navLink-${index}`} role="listitem" > <a href={getAbsoluteLink(navigationLink)} className="header-links"> {eval(navigationText)} </a> </li> ); } renderNavList() { const { data } = this.props; const { showMobileMenu } = this.state; const navigationLinks = _.get(data, "variables.navigation.value", []); const navListClassname = showMobileMenu ? "nav-list header-navigation nav-list-mobile" : "nav-list header-navigation"; return ( <ul id="header-navigation" className={navListClassname} data-header-navigation role="menu" > {navigationLinks.map(this.renderNavListItem)} <LanguageSelect className="language-select" /> {this.renderLoginOrUserMenu("desktop")} </ul> ); } renderDesktopNav() { return ( <nav className="nav"> <div className="container"> {this.renderLogo()} <div className="nav-flex">{this.renderNavList()}</div> </div> </nav> ); } render() { const { showDesktopMenu } = this.state; return showDesktopMenu ? this.renderDesktopNav() : this.renderMobileNav(); } } Nav.defaultProps = { data: {}, settings: {}, }; return React.createElement(Nav, { data, settings }); })()} </React.Fragment>;
render() { const { showDesktopMenu } = this.state; return showDesktopMenu ? this.renderDesktopNav() : this.renderMobileNav(); } } Nav.defaultProps = { data: {}, settings: {}, }; return React.createElement(Nav, { data, settings }); })()} </React.Fragment>;
Update the manifest.json file
Open the manifest.json file and replace the code in the Advanced section with:
{ "version": 2, "design": { "name": "design", "variables": { "font": { "type": "section", "name": "font", "label": "", "variables": { "headingsAndButtonsFont": { "type": "select", "name": "headingsAndButtonsFont", "label": "", "enum": [ { "label": "Arial", "value": "arial" }, { "label": "Verdana", "value": "verdana" }, { "label": "Helvetica", "value": "helvetica" }, { "label": "Tahoma", "value": "tahoma" }, { "label": "Times New Roman", "value": "times-new-roman" }, { "label": "Georgia", "value": "georgia" }, { "label": "Garamond", "value": "garamond" }, { "label": "Courier New", "value": "courier-new" } ], "value": "Helvetica" }, "headingBaseSizeFont": { "type": "slider", "name": "headingBaseSizeFont", "label": "", "value": "36px" }, "bodyTextFont": { "type": "select", "name": "bodyTextFont", "label": "", "enum": [ { "label": "Arial", "value": "arial" }, { "label": "Verdana", "value": "verdana" }, { "label": "Helvetica", "value": "helvetica" }, { "label": "Tahoma", "value": "tahoma" }, { "label": "Times New Roman", "value": "times-new-roman" }, { "label": "Georgia", "value": "georgia" }, { "label": "Garamond", "value": "garamond" }, { "label": "Courier New", "value": "courier-new" } ], "value": "Helvetica" }, "baseSizeFont": { "type": "slider", "name": "baseSizeFont", "label": "", "value": "16px" } } }, "colors": { "type": "section", "name": "colors", "label": "", "variables": { "font": { "type": "group", "name": "font", "label": "", "variables": { "headingsColor": { "type": "color", "name": "headingsColor", "label": "", "value": "#0A3355" }, "bodyTextColor": { "type": "color", "name": "bodyTextColor", "label": "", "value": "#222222" }, "secondaryTextColor": { "type": "color", "name": "secondaryTextColor", "label": "", "value": "#576977" }, "linksColor": { "type": "color", "name": "linksColor", "label": "", "value": "#0A3355" }, "linkTextHoverColor": { "type": "color", "name": "linkTextHoverColor", "label": "", "value": "#005FAD" } } }, "buttons": { "type": "group", "name": "buttons", "label": "", "variables": { "primaryButtonColor": { "type": "color", "name": "primaryButtonColor", "label": "", "value": "#0A3355" }, "primaryButtonTextColor": { "type": "color", "name": "primaryButtonTextColor", "label": "", "value": "#ffffff" }, "primaryButtonBackgroundHoverColor": { "type": "color", "name": "primaryButtonBackgroundHoverColor", "label": "", "value": "#005FAD" }, "primaryButtonTextHoverColor": { "type": "color", "name": "primaryButtonTextHoverColor", "label": "", "value": "#ffffff" } } }, "page": { "type": "group", "name": "page", "label": "", "variables": { "primaryBackgroundPageColor": { "type": "color", "name": "primaryBackgroundPageColor", "label": "", "value": "#ffffff" }, "secondaryBackgroundPageColor": { "type": "color", "name": "secondaryBackgroundPageColor", "label": "", "value": "#F9F9F9" }, "borderPageColor": { "type": "color", "name": "borderPageColor", "label": "", "value": "#B2B2B2" } } } } } } }, "pages": { "homepage": { "type": "page", "name": "homepage", "variables": { "announcement": { "type": "section", "name": "announcement", "label": "[[ sn.kustomer.themebuilder.announcement_label ]]", "enabled": true, "variables": { "homepageOnly": { "type": "boolean", "name": "homepageOnly", "label": "[[ sn.kustomer.themebuilder.announcement_show_on_home_page_label ]]", "value": false }, "announcementText": { "type": "text", "name": "announcementText", "label": "[[ sn.kustomer.themebuilder.announcement_text_value ]]", "value": "Important update about our holiday hours" }, "textColor": { "type": "color", "name": "textColor", "label": "[[ sn.kustomer.themebuilder.announcement_text_color_label ]]", "value": "#FFFFFF" }, "backgroundColor": { "type": "color", "name": "backgroundColor", "label": "[[ sn.kustomer.themebuilder.announcement_background_color_label ]]", "value": "#E16360" }, "ctaText": { "type": "string", "name": "ctaText", "label": "[[ sn.kustomer.themebuilder.cta_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.announcement_cta_text_value\" />" }, "ctaTextColor": { "type": "color", "name": "ctaTextColor", "label": "[[ sn.kustomer.themebuilder.announcement_cta_text_color_label ]]", "value": "#FFFFFF" }, "ctaLink": { "type": "link", "name": "ctaLink", "label": "[[ sn.kustomer.themebuilder.cta_link_label ]]", "value": "" } } }, "header": { "type": "section", "name": "header", "label": "[[ sn.kustomer.themebuilder.header_label ]]", "link": { "label": "[[ sn.kustomer.themebuilder.header_link_label ]]", "description": "[[ sn.kustomer.themebuilder.header_link_description ]]", "buttonText": "[[ sn.kustomer.themebuilder.header_link_button_text ]]", "value": "/app/settings/kb/config" }, "variables": { "navigation": { "type": "list", "name": "navigation", "minItems": 1, "maxItems": 5, "label": "[[ sn.kustomer.themebuilder.header_navigation_label ]]", "buttonText": "[[ sn.kustomer.themebuilder.header_navigation_button_text ]]", "value": [ { "navigationText": { "type": "string", "name": "navigationText", "label": "[[ sn.kustomer.themebuilder.navigation_text_label_1 ]]", "value": "test" }, "navigationLink": { "type": "link", "name": "navigationLink", "label": "[[ sn.kustomer.themebuilder.navigation_link_label_1 ]]", "value": "" } }, { "navigationText": { "type": "string", "name": "navigationText", "label": "[[ sn.kustomer.themebuilder.navigation_text_label_1 ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.navigation_text_value_1\" />" }, "navigationLink": { "type": "link", "name": "navigationLink", "label": "[[ sn.kustomer.themebuilder.navigation_link_label_1 ]]", "value": "" } } ] } } }, "searchBar": { "type": "section", "name": "searchBar", "label": "[[ sn.kustomer.themebuilder.search_bar_label ]]", "variables": { "primaryMessage": { "type": "string", "name": "primaryMessage", "label": "[[ sn.kustomer.themebuilder.search_bar_primary_message_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.search_bar_primary_message_value\" />" }, "secondaryMessage": { "type": "text", "name": "secondaryMessage", "label": "[[ sn.kustomer.themebuilder.search_bar_secondary_message_label ]]", "value": "Everything you need to know about Acme Corp" }, "overlayTextColor": { "type": "color", "name": "overlayTextColor", "label": "[[ sn.kustomer.themebuilder.search_bar_overlay_text_color_label ]]", "value": "#FFFFFF" }, "promptText": { "type": "string", "name": "promptText", "label": "[[ sn.kustomer.themebuilder.search_bar_prompt_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.search_bar_prompt_text_value\" />" }, "heroImage": { "type": "file", "name": "heroImage", "label": "[[ sn.kustomer.themebuilder.search_bar_hero_image_label ]]", "value": "https://cdnkb.kustomerapp.com/templates/midtown/images/150x150transparent.png" }, "overlayColor": { "type": "color", "name": "overlayColor", "label": "[[ sn.kustomer.themebuilder.search_bar_overlay_color_label ]]", "value": "#0a3355" }, "overlayOpacity": { "type": "slider", "name": "overlayOpacity", "label": "[[ sn.kustomer.themebuilder.search_bar_overlay_opacity_label ]]", "value": 10 } } }, "category": { "type": "section", "name": "category", "label": "[[ sn.kustomer.themebuilder.category_label ]]", "enabled": true, "link": { "label": "[[ sn.kustomer.themebuilder.category_link_label ]]", "description": "[[ sn.kustomer.themebuilder.category_link_description ]]", "learnMoreLink": "https://help.kustomer.com/brands-and-categories-rJEANXXsB#CategoryIcon", "buttonText": "[[ sn.kustomer.themebuilder.category_link_button_text ]]", "value": "/app/settings/kb/categories" }, "variables": { "primaryText": { "type": "string", "name": "primaryText", "label": "[[ sn.kustomer.themebuilder.primary_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.category_primary_text_value\" />" }, "secondaryText": { "type": "text", "name": "secondaryText", "label": "[[ sn.kustomer.themebuilder.secondary_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.category_secondary_text_value\" />" } } }, "featuredArticles": { "type": "section", "name": "featuredArticles", "label": "[[ sn.kustomer.themebuilder.featured_articles_label ]]", "enabled": true, "link": { "label": "[[ sn.kustomer.themebuilder.featured_articles_link_label ]]", "description": "[[ sn.kustomer.themebuilder.featured_articles_link_description ]]", "learnMoreLink": "https://help.kustomer.com/featured-articles-SkWJSGJbO", "buttonText": "[[ sn.kustomer.themebuilder.featured_articles_link_button_text ]]", "value": "/app/settings/kb/articles" }, "variables": { "primaryText": { "type": "string", "name": "primaryText", "label": "[[ sn.kustomer.themebuilder.primary_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.featured_articles_label\" />" }, "secondaryText": { "type": "text", "name": "secondaryText", "label": "[[ sn.kustomer.themebuilder.secondary_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.featured_articles_secondary_text_value\" />" } } }, "contactUs": { "type": "section", "name": "contactUs", "label": "[[ sn.kustomer.themebuilder.contact_us_label ]]", "enabled": true, "variables": { "primaryText": { "type": "string", "name": "primaryText", "label": "[[ sn.kustomer.themebuilder.primary_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.contact_us_primary_text_label\" />" }, "secondaryText": { "type": "text", "name": "secondaryText", "label": "[[ sn.kustomer.themebuilder.secondary_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.contact_us_secondary_text_value\" />" }, "ctaText": { "type": "string", "name": "ctaText", "label": "[[ sn.kustomer.themebuilder.cta_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.contact_us_label\" />" }, "ctaLink": { "type": "link", "name": "ctaLink", "label": "[[ sn.kustomer.themebuilder.cta_link_label ]]", "value": "" } } }, "footer": { "type": "section", "name": "footer", "label": "[[ sn.kustomer.themebuilder.footer_label ]]", "variables": { "navigation": { "type": "list", "name": "navigation", "minItems": 1, "maxItems": 5, "collapsible": true, "label": "[[ sn.kustomer.themebuilder.footer_navigation_label ]]", "buttonText": "[[ sn.kustomer.themebuilder.footer_navigation_button_text ]]", "value": [ { "navigationText": { "type": "string", "name": "navigationText", "label": "[[ sn.kustomer.themebuilder.navigation_text_label_1 ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.navigation_text_value_1\" />" }, "navigationLink": { "type": "link", "name": "navigationLink", "label": "[[ sn.kustomer.themebuilder.navigation_link_label_1 ]]", "value": "" } } ] }, "social": { "type": "list", "name": "social", "minItems": 1, "maxItems": 5, "collapsible": true, "label": "[[ sn.kustomer.themebuilder.footer_social_label ]]", "buttonText": "[[ sn.kustomer.themebuilder.footer_social_button_text ]]", "value": [ { "socialIcon": { "type": "icon", "name": "socialIcon", "label": "[[ sn.kustomer.themebuilder.footer_social_icon_label ]]", "enum": [ { "label": "facebook", "value": "facebook" }, { "label": "instagram", "value": "instagram" }, { "label": "twitter", "value": "twitter" }, { "label": "pinterest", "value": "pinterest" }, { "label": "linkedin", "value": "linkedin" } ], "value": "facebook" }, "socialLink": { "type": "link", "name": "socialLink", "label": "[[ sn.kustomer.themebuilder.footer_social_link_label ]]", "value": "" } } ] }, "copyrightInfo": { "type": "string", "name": "copyrightInfo", "label": "[[ sn.kustomer.themebuilder.footer_copyright_label ]]", "value": "All rights reserved 2020" }, "footerPrimaryTextColor": { "type": "color", "name": "footerPrimaryTextColor", "label": "[[ sn.kustomer.themebuilder.footer_primary_text_color_label ]]", "value": "#FFFFFF" }, "footerSecondaryTextColor": { "type": "color", "name": "footerSecondaryTextColor", "label": "[[ sn.kustomer.themebuilder.footer_secondary_text_color_label ]]", "value": "#939FA8" }, "footerBackgroundColor": { "type": "color", "name": "footerBackgroundColor", "label": "[[ sn.kustomer.themebuilder.footer_background_color_label ]]", "value": "#0a3355" } } } } }, "conversations": { "type": "page", "name": "conversations", "label": "[[ sn.kustomer.themebuilder.conversations_label ]]", "variables": { "primaryText": { "type": "string", "name": "primaryText", "label": "[[ sn.kustomer.themebuilder.primary_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.conversations_primary_text_value\" />" }, "secondaryText": { "type": "text", "name": "secondaryText", "label": "[[ sn.kustomer.themebuilder.secondary_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.conversations_secondary_text_value\" />" }, "openStatusTextColor": { "type": "color", "name": "openStatusTextColor", "label": "[[ sn.kustomer.themebuilder.conversations_open_status_text_color_label ]]", "value": "#15CC70" }, "openStatusBackgroundColor": { "type": "color", "name": "openStatusBackgroundColor", "label": "[[ sn.kustomer.themebuilder.conversations_open_status_background_color_label ]]", "value": "#e8faf1" }, "doneStatusTextColor": { "type": "color", "name": "doneStatusTextColor", "label": "[[ sn.kustomer.themebuilder.conversations_done_status_text_color_label ]]", "value": "#767676" }, "doneStatusBackgroundColor": { "type": "color", "name": "doneStatusBackgroundColor", "label": "[[ sn.kustomer.themebuilder.conversations_done_status_background_color_label ]]", "value": "#f1f1f1" }, "statusFilterBackgroundColor": { "type": "color", "name": "statusFilterBackgroundColor", "label": "[[ sn.kustomer.themebuilder.conversations_status_filter_background_color_label ]]", "value": "#FFFFFF" }, "statusFilterSelectedBackgroundColor": { "type": "color", "name": "statusFilterSelectedBackgroundColor", "label": "[[ sn.kustomer.themebuilder.conversations_status_filter_selected_background_color_label ]]", "value": "#E3E3E3" } } }, "conversation": { "type": "page", "name": "conversation", "label": "[[ sn.kustomer.themebuilder.conversation_label ]]", "variables": { "detailsText": { "type": "text", "name": "detailsText", "label": "[[ sn.kustomer.themebuilder.conversation_details_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.conversation_details_text_value\" />" }, "sendButtonText": { "type": "text", "name": "sendButtonText", "label": "[[ sn.kustomer.themebuilder.conversation_send_button_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.conversation_send_button_text_value\" />" }, "messageFailedText": { "type": "text", "name": "messageFailedText", "label": "[[ sn.kustomer.themebuilder.conversation_message_failed_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.message_error_text\" />" }, "messagePendingText": { "type": "text", "name": "messagePendingText", "label": "[[ sn.kustomer.themebuilder.conversation_message_pending_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.message_pending_text\" />" }, "replyBoxPlaceholderText": { "type": "text", "name": "replyBoxPlaceholderText", "label": "[[ sn.kustomer.themebuilder.conversation_details_reply_placeholder_text_label ]]", "value": "<Snippet id=\"sn.kustomer.themebuilder.conversation_details_reply_placeholder_text_value\" />" }, "userMessageBackgroundColor": { "type": "color", "name": "userMessageBackgroundColor", "label": "[[ sn.kustomer.themebuilder.conversation_user_message_background_color_label ]]", "value": "#F4F3F8" }, "userMessageTextColor": { "type": "color", "name": "userMessageTextColor", "label": "[[ sn.kustomer.themebuilder.conversation_user_message_text_color_label ]]", "value": "#000000" }, "agentMessageBackgroundColor": { "type": "color", "name": "agentMessageBackgroundColor", "label": "[[ sn.kustomer.themebuilder.conversation_agent_message_background_color_label ]]", "value": "#FFF1F0" }, "agentMessageTextColor": { "type": "color", "name": "agentMessageTextColor", "label": "[[ sn.kustomer.themebuilder.conversation_agent_message_text_color_label ]]", "value": "#000000" } } } } }
Note: Please ensure you’ve copied over values from your existing manifest to the updated manifest data structure. For example, if the existing value of your announcementText
is 50% off!
, make sure to copy this value over to the updated data structure:
"announcementText": { "name": "announcementText", "type": "text", "label": "[[ sn.kustomer.themebuilder.announcement_text_value ]]", "value": "50% off! " },
Update the ArticleItem.jsx file
- Open the ArticleItem.jsx file, find <h2 className="article-item-header">{title}</h2> and replace that code with:
<h2 className="article-item-header"> {title} {scope === 'internal' && ( <Tooltip content={<Snippet id="sn.kustomer.themebuilder.internal_article_icon_tooltip" />}> <i className="mdi mdi-lock article-item-header-icon" aria-hidden="true" /> </Tooltip> )} </h2>
Update the SearchItem.jsx file
- Open the SearchItem.jsx file and make the following changes:
- Add
scope
to the default properties listed underarticleId
on lines 97 and 111 - Find <h2 className="article-item-header bold">{title}</h2> and replace that code with:
<h2 className="article-item-header bold"> {title} {scope === 'internal' && ( <Tooltip content={<Snippet id="sn.kustomer.themebuilder.internal_article_icon_tooltip" />}> <i className="mdi mdi-lock article-item-header-icon" aria-hidden="true" /> </Tooltip> )} </h2>
- Add
Update the article.jsx file
- Open the article.jsx file, find <h2 className="article-title">{article.title}</h2> and replace that code with:
<h2 className="article-title"> {article.title} {article.scope === 'internal' && ( <Tooltip content={<Snippet id="sn.kustomer.themebuilder.internal_article_icon_tooltip" />}> <i className="mdi mdi-lock article-item-header-icon" aria-hidden="true" /> </Tooltip> )} </h2>
You now have access to the Portal.