* [feature] help search modal * add symlinks for docs assets * update help links * [ui] fix help dropdown in mobile view * replaced {index} tags in help content, api for fetching help content * help links open in help modal * moved help content logic to pyversion-14
@@ -57,14 +57,19 @@ def make_asset_dirs(make_copy=False): | |||
# symlink app/public > assets/app | |||
for app_name in frappe.get_all_apps(True): | |||
pymodule = frappe.get_module(app_name) | |||
source = os.path.join(os.path.abspath(os.path.dirname(pymodule.__file__)), 'public') | |||
target = os.path.join(assets_path, app_name) | |||
if not os.path.exists(target) and os.path.exists(source): | |||
if make_copy: | |||
shutil.copytree(os.path.abspath(source), target) | |||
else: | |||
os.symlink(os.path.abspath(source), target) | |||
app_base_path = os.path.abspath(os.path.dirname(pymodule.__file__)) | |||
symlinks = [] | |||
symlinks.append([os.path.join(app_base_path, 'public'), os.path.join(assets_path, app_name)]) | |||
symlinks.append([os.path.join(app_base_path, 'docs'), os.path.join(assets_path, app_name + '_docs')]) | |||
for source, target in symlinks: | |||
source = os.path.abspath(source) | |||
if not os.path.exists(target) and os.path.exists(source): | |||
if make_copy: | |||
shutil.copytree(source, target) | |||
else: | |||
os.symlink(source, target) | |||
def build(no_compress=False, verbose=False): | |||
assets_path = os.path.join(frappe.local.sites_path, "assets") | |||
@@ -1,6 +1,6 @@ | |||
Often while building javascript interfaces, there is a need to render DOM as an HTML template. Frappe Framework uses John Resig's Microtemplate script to render HTML templates in the Desk application. | |||
> Note 1: In Frappe we use the Jinja-like `{%` tags to embed code rather than the standard `<%` | |||
> Note 1: In Frappe we use the Jinja-like `{% raw %}{%{% endraw %}` tags to embed code rather than the standard `<%` | |||
> Note 2: Never use single quotes `'` inside the HTML template. | |||
@@ -65,6 +65,6 @@ Here is what the report looks like: | |||
1. [Bootstrap Stylesheet](http://getbootstrap.com) is pre-loaded. | |||
1. You can use all global functions like `fmt_money` and dateutil. | |||
1. Translatable strings should be written as `__("text")` | |||
1. You can create modules and import using `{% include "templates/includes/formats/common_format" %}` | |||
1. You can create modules and import using `{% raw %}{% include "templates/includes/formats/common_format" %}{% endraw %}` | |||
<!-- markdown --> |
@@ -1,6 +1,6 @@ | |||
Often while building javascript interfaces, there is a need to render DOM as an HTML template. Frappe Framework uses John Resig's Microtemplate script to render HTML templates in the Desk application. | |||
> Note 1: In Frappe we use the Jinja-like `{%` tags to embed code rather than the standard `<%` | |||
> Note 1: In Frappe we use the Jinja-like `{% raw %}{%{% endraw %}` tags to embed code rather than the standard `<%` | |||
> Note 2: Never use single quotes `'` inside the HTML template. | |||
@@ -65,6 +65,6 @@ Here is what the report looks like: | |||
1. [Bootstrap Stylesheet](http://getbootstrap.com) is pre-loaded. | |||
1. You can use all global functions like `fmt_money` and dateutil. | |||
1. Translatable strings should be written as `__("text")` | |||
1. You can create modules and import using `{% include "templates/includes/formats/common_format" %}` | |||
1. You can create modules and import using `{% raw %}{% include "templates/includes/formats/common_format" %}{% endraw %}` | |||
<!-- markdown --> |
@@ -2,5 +2,5 @@ | |||
Select your language | |||
1. [English](en) | |||
1. [Français](fr) | |||
1. [English]({{docs_base_url}}/user/en) | |||
1. [Français]({{docs_base_url}}/user/fr) |
@@ -115,6 +115,7 @@ | |||
"public/js/frappe/misc/datetime.js", | |||
"public/js/frappe/misc/number_format.js", | |||
"public/js/frappe/misc/help.js", | |||
"public/js/frappe/misc/help_links.js", | |||
"public/js/frappe/ui/upload.html", | |||
"public/js/frappe/upload.js", | |||
@@ -102,6 +102,12 @@ kbd { | |||
font-size: 12px; | |||
border-radius: 0px 0px 4px 4px; | |||
} | |||
.dropdown-menu .dropdown-header { | |||
padding: 3px 14px; | |||
font-size: 11px; | |||
font-weight: 200; | |||
padding-top: 12px; | |||
} | |||
.dropdown-menu .divider { | |||
margin: 0px; | |||
} | |||
@@ -102,6 +102,12 @@ kbd { | |||
font-size: 12px; | |||
border-radius: 0px 0px 4px 4px; | |||
} | |||
.dropdown-menu .dropdown-header { | |||
padding: 3px 14px; | |||
font-size: 11px; | |||
font-weight: 200; | |||
padding-top: 12px; | |||
} | |||
.dropdown-menu .divider { | |||
margin: 0px; | |||
} | |||
@@ -582,3 +588,43 @@ fieldset[disabled] .form-control { | |||
.liked-by-popover li { | |||
margin: 15px 0px; | |||
} | |||
.screenshot { | |||
border: 1px solid #d1d8dd; | |||
box-shadow: 1px 1px 7px rgba(0, 0, 0, 0.15); | |||
margin: 8px 0px; | |||
max-width: 100%; | |||
} | |||
.help-modal a { | |||
color: #5E64FF; | |||
} | |||
.help-modal .modal-dialog { | |||
width: 768px; | |||
} | |||
.help-modal .modal-body { | |||
padding: 15px 27px; | |||
} | |||
.help-modal .parent-link { | |||
line-height: 4em; | |||
} | |||
.help-modal .parent-link:before { | |||
font-family: 'Octicons'; | |||
content: '\f0a4'; | |||
} | |||
.help-modal .edit-container { | |||
padding-bottom: 12px; | |||
} | |||
@media (max-width: 767px) { | |||
.help-modal .modal-dialog { | |||
width: auto; | |||
} | |||
.help-modal .modal-content { | |||
height: auto !important; | |||
} | |||
.help-modal iframe { | |||
height: auto; | |||
width: 100%; | |||
} | |||
} | |||
.search-result { | |||
margin-bottom: 24px; | |||
} |
@@ -29,7 +29,7 @@ | |||
} | |||
@media (max-width: 991px) { | |||
.navbar-desk { | |||
width: 40% !important; | |||
width: 35% !important; | |||
} | |||
.navbar-desk ~ ul > li { | |||
float: left; | |||
@@ -48,7 +48,7 @@ | |||
} | |||
@media (max-width: 767px) { | |||
.navbar-desk { | |||
width: 60% !important; | |||
width: 50% !important; | |||
} | |||
} | |||
#search-modal .modal-dialog, | |||
@@ -76,9 +76,37 @@ | |||
.dropdown-navbar-new-comments .dropdown-menu { | |||
margin-top: 0; | |||
} | |||
.dropdown-help .dropdown-menu { | |||
width: 350px !important; | |||
max-height: 440px; | |||
overflow: scroll; | |||
} | |||
.dropdown-help .dropdown-menu .input-group { | |||
width: 100%; | |||
background-color: #f5f7fa; | |||
padding: 8px 12px; | |||
} | |||
.dropdown-help .dropdown-menu input { | |||
width: 100%; | |||
padding: 5px 10px; | |||
outline: none; | |||
border-radius: 3px 0 0 3px; | |||
border: 1px solid #d1d8dd; | |||
opacity: 0.9; | |||
line-height: 1.5; | |||
} | |||
.dropdown-help .dropdown-menu button { | |||
border: 1px solid #d1d8dd; | |||
} | |||
@media (max-width: 767px) { | |||
.dropdown-navbar-new-comments.open .dropdown-menu, | |||
.dropdown-navbar-user.open .dropdown-menu { | |||
.dropdown-help .dropdown-menu { | |||
position: fixed !important; | |||
top: 40px; | |||
width: 100% !important; | |||
} | |||
} | |||
@media (max-width: 767px) { | |||
.dropdown-mobile.open .dropdown-menu { | |||
position: absolute; | |||
border-top: 1px solid rgba(0, 0, 0, 0.14902); | |||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); | |||
@@ -86,8 +114,7 @@ | |||
right: 0; | |||
left: auto; | |||
} | |||
.dropdown-navbar-new-comments.open .dropdown-menu > li > a, | |||
.dropdown-navbar-user.open .dropdown-menu > li > a { | |||
.dropdown-mobile.open .dropdown-menu > li > a { | |||
padding: 12px; | |||
} | |||
} | |||
@@ -107,6 +134,11 @@ | |||
width: 300px; | |||
background-color: rgba(255, 255, 255, 0.9); | |||
} | |||
@media (max-width: 991px) { | |||
#navbar-search { | |||
width: 250px; | |||
} | |||
} | |||
.navbar .navbar-search-icon { | |||
color: #6C7680; | |||
font-size: inherit; | |||
@@ -502,12 +534,6 @@ p { | |||
border: 1px solid #d1d8dd; | |||
border-radius: 15px; | |||
} | |||
.screenshot { | |||
border: 1px solid #d1d8dd; | |||
box-shadow: 1px 1px 7px rgba(0, 0, 0, 0.15); | |||
margin: 8px 0px; | |||
max-width: 100%; | |||
} | |||
hr { | |||
margin: 48px 0px 30px; | |||
} | |||
@@ -29,7 +29,7 @@ | |||
} | |||
@media (max-width: 991px) { | |||
.navbar-desk { | |||
width: 40% !important; | |||
width: 35% !important; | |||
} | |||
.navbar-desk ~ ul > li { | |||
float: left; | |||
@@ -48,7 +48,7 @@ | |||
} | |||
@media (max-width: 767px) { | |||
.navbar-desk { | |||
width: 60% !important; | |||
width: 50% !important; | |||
} | |||
} | |||
#search-modal .modal-dialog, | |||
@@ -76,9 +76,37 @@ | |||
.dropdown-navbar-new-comments .dropdown-menu { | |||
margin-top: 0; | |||
} | |||
.dropdown-help .dropdown-menu { | |||
width: 350px !important; | |||
max-height: 440px; | |||
overflow: scroll; | |||
} | |||
.dropdown-help .dropdown-menu .input-group { | |||
width: 100%; | |||
background-color: #f5f7fa; | |||
padding: 8px 12px; | |||
} | |||
.dropdown-help .dropdown-menu input { | |||
width: 100%; | |||
padding: 5px 10px; | |||
outline: none; | |||
border-radius: 3px 0 0 3px; | |||
border: 1px solid #d1d8dd; | |||
opacity: 0.9; | |||
line-height: 1.5; | |||
} | |||
.dropdown-help .dropdown-menu button { | |||
border: 1px solid #d1d8dd; | |||
} | |||
@media (max-width: 767px) { | |||
.dropdown-navbar-new-comments.open .dropdown-menu, | |||
.dropdown-navbar-user.open .dropdown-menu { | |||
.dropdown-help .dropdown-menu { | |||
position: fixed !important; | |||
top: 40px; | |||
width: 100% !important; | |||
} | |||
} | |||
@media (max-width: 767px) { | |||
.dropdown-mobile.open .dropdown-menu { | |||
position: absolute; | |||
border-top: 1px solid rgba(0, 0, 0, 0.14902); | |||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); | |||
@@ -86,8 +114,7 @@ | |||
right: 0; | |||
left: auto; | |||
} | |||
.dropdown-navbar-new-comments.open .dropdown-menu > li > a, | |||
.dropdown-navbar-user.open .dropdown-menu > li > a { | |||
.dropdown-mobile.open .dropdown-menu > li > a { | |||
padding: 12px; | |||
} | |||
} | |||
@@ -107,6 +134,11 @@ | |||
width: 300px; | |||
background-color: rgba(255, 255, 255, 0.9); | |||
} | |||
@media (max-width: 991px) { | |||
#navbar-search { | |||
width: 250px; | |||
} | |||
} | |||
.navbar .navbar-search-icon { | |||
color: #6C7680; | |||
font-size: inherit; | |||
@@ -102,6 +102,12 @@ kbd { | |||
font-size: 12px; | |||
border-radius: 0px 0px 4px 4px; | |||
} | |||
.dropdown-menu .dropdown-header { | |||
padding: 3px 14px; | |||
font-size: 11px; | |||
font-weight: 200; | |||
padding-top: 12px; | |||
} | |||
.dropdown-menu .divider { | |||
margin: 0px; | |||
} | |||
@@ -0,0 +1,51 @@ | |||
frappe.provide('frappe.help.help_links'); | |||
frappe.help.help_links['data-import-tool'] = [ | |||
{ label: 'Importing and Exporting Data', url: 'https://frappe.github.io/erpnext/user/manual/en/setting-up/data/data-import-tool.html' }, | |||
] | |||
frappe.help.help_links['modules/Setup'] = [ | |||
{ label: 'Users and Permissions', url: 'http://frappe.github.io/erpnext/user/manual/en/setting-up/users-and-permissions/' }, | |||
{ label: 'Settings', url: 'http://frappe.github.io/erpnext/user/manual/en/setting-up/settings/' }, | |||
{ label: 'Data Management', url: 'http://frappe.github.io/erpnext/user/manual/en/setting-up/data/' }, | |||
{ label: 'Email', url: 'http://frappe.github.io/erpnext/user/manual/en/setting-up/email/' }, | |||
{ label: 'Printing', url: 'http://frappe.github.io/erpnext/user/manual/en/setting-up/print/' }, | |||
] | |||
frappe.help.help_links['List/User'] = [ | |||
{ label: 'Adding Users', url: 'https://frappe.github.io/erpnext/user/manual/en/setting-up/users-and-permissions/adding-users' }, | |||
{ label: 'Rename User', url: 'https://frappe.github.io/erpnext/user/manual/en/setting-up/articles/rename-user' }, | |||
] | |||
frappe.help.help_links['permission-manager'] = [ | |||
{ label: 'Role Permissions Manager', url: 'https://frappe.github.io/erpnext/user/manual/en/setting-up/users-and-permissions/role-based-permissions' }, | |||
] | |||
frappe.help.help_links['user-permissions'] = [ | |||
{ label: 'User Permissions', url: 'https://frappe.github.io/erpnext/user/manual/en/setting-up/users-and-permissions/user-permissions' }, | |||
] | |||
frappe.help.help_links['Form/System Settings'] = [ | |||
{ label: 'System Settings', url: 'https://frappe.github.io/erpnext/user/manual/en/setting-up/settings/system-settings' }, | |||
] | |||
frappe.help.help_links['modules_setup'] = [ | |||
{ label: 'Show or Hide Desktop Icons', url: 'https://frappe.github.io/erpnext/user/manual/en/setting-up/settings/module-settings' }, | |||
] | |||
frappe.help.help_links['List/Email Account'] = [ | |||
{ label: 'Email Account', url: 'https://frappe.github.io/erpnext/user/manual/en/setting-up/email/email-account' }, | |||
] | |||
frappe.help.help_links['List/Email Alert'] = [ | |||
{ label: 'Email Alert', url: 'https://frappe.github.io/erpnext/user/manual/en/setting-up/email/email-alerts' }, | |||
] | |||
frappe.help.help_links['Form/Print Settings'] = [ | |||
{ label: 'Print Settings', url: 'https://frappe.github.io/erpnext/user/manual/en/setting-up/print/print-settings' }, | |||
] | |||
frappe.help.help_links['print-format-builder'] = [ | |||
{ label: 'Print Format Builder', url: 'https://frappe.github.io/erpnext/user/manual/en/setting-up/print/print-format-builder' }, | |||
] | |||
@@ -14,7 +14,7 @@ | |||
<li class="visible-xs"> | |||
<a class="navbar-search-button" href="#" data-toggle="modal" data-target="#search-modal"><i class="octicon octicon-search"></i></a> | |||
</li> | |||
<li class="dropdown dropdown-navbar-user"> | |||
<li class="dropdown dropdown-navbar-user dropdown-mobile"> | |||
<a class="dropdown-toggle" data-toggle="dropdown" href="#" | |||
onclick="return false;"> | |||
{{ avatar }} | |||
@@ -44,7 +44,31 @@ | |||
{%= __("Logout") %}</a></li> | |||
</ul> | |||
</li> | |||
<li class="dropdown dropdown-navbar-new-comments"> | |||
<li class="dropdown dropdown-help dropdown-mobile"> | |||
<a class="dropdown-toggle" data-toggle="dropdown" href="#" | |||
onclick="return false;" style="height: 40px;"> | |||
<span class="hidden-xs hidden-sm" style="vertical-align: middle;">Help <b class="caret"></b></span> | |||
<span class="visible-xs visible-sm standard-image" | |||
style="padding: 50% 7px; font-size: 17px; background-color: #fafbfc; font-weight: 100;">?</span> | |||
</a> | |||
<ul class="dropdown-menu" role="menu"> | |||
<div class="input-group" style="border-bottom: 1px solid #d1d8dd;"> | |||
<input id="input-help" type="text" placeholder="What do you need help with?" autofocus> | |||
<span class="input-group-btn"><button class="btn btn-default">Go</button></span> | |||
</div> | |||
<li id="help-links"></li> | |||
<li class="divider"></li> | |||
<li> | |||
<a data-path="/user/manual/index" target="_blank">Browse the manual</a> | |||
<a href="https://discuss.erpnext.com" target="_blank">Explore the forums</a> | |||
<a href="https://gitter.im/frappe/erpnext" target="_blank">Chat</a> | |||
<a href="mailto:hello@erpnext.com" target="_blank">Email us</a> | |||
</li> | |||
</ul> | |||
</li> | |||
<li class="dropdown dropdown-navbar-new-comments dropdown-mobile"> | |||
<a class="btn dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> | |||
<span class="navbar-new-comments">0</span> | |||
</a> | |||
@@ -9,12 +9,40 @@ frappe.ui.toolbar.Toolbar = Class.extend({ | |||
avatar: frappe.avatar(frappe.session.user) | |||
})); | |||
this.setup_sidebar(); | |||
$(document).on("notification-update", function() { | |||
frappe.ui.notifications.update_notifications(); | |||
}); | |||
$('.dropdown-toggle').dropdown(); | |||
this.setup_help(); | |||
$(document).trigger('toolbar_setup'); | |||
// clear all custom menus on page change | |||
$(document).on("page-change", function() { | |||
$("header .navbar .custom-menu").remove(); | |||
}); | |||
frappe.search.setup(); | |||
}, | |||
setup_sidebar: function () { | |||
var header = $('header'); | |||
header.find(".toggle-sidebar").on("click", function () { | |||
var layout_side_section = $('.layout-side-section'); | |||
var overlay_sidebar = layout_side_section.find('.overlay-sidebar'); | |||
overlay_sidebar.addClass('opened'); | |||
overlay_sidebar.find('.reports-dropdown').removeClass('dropdown-menu').addClass('list-unstyled'); | |||
overlay_sidebar.find('.dropdown-toggle').addClass('text-muted').find('.caret').addClass('hidden-xs hidden-sm'); | |||
overlay_sidebar.find('.reports-dropdown') | |||
.removeClass('dropdown-menu') | |||
.addClass('list-unstyled'); | |||
overlay_sidebar.find('.dropdown-toggle') | |||
.addClass('text-muted').find('.caret') | |||
.addClass('hidden-xs hidden-sm'); | |||
$('<div class="close-sidebar">').hide().appendTo(layout_side_section).fadeIn(); | |||
@@ -28,28 +56,149 @@ frappe.ui.toolbar.Toolbar = Class.extend({ | |||
scroll_container.css("overflow-y", ""); | |||
layout_side_section.find(".close-sidebar").fadeOut(function() { | |||
overlay_sidebar.removeClass('opened').find('.dropdown-toggle').removeClass('text-muted'); | |||
overlay_sidebar.find('.reports-dropdown').addClass('dropdown-menu'); | |||
overlay_sidebar.removeClass('opened') | |||
.find('.dropdown-toggle') | |||
.removeClass('text-muted'); | |||
overlay_sidebar.find('.reports-dropdown') | |||
.addClass('dropdown-menu'); | |||
}); | |||
} | |||
}); | |||
}, | |||
$(document).on("notification-update", function() { | |||
frappe.ui.notifications.update_notifications(); | |||
setup_help: function () { | |||
$(".dropdown-help .dropdown-toggle").on("click", function () { | |||
$(".dropdown-help input").focus(); | |||
}); | |||
$('.dropdown-toggle').dropdown(); | |||
$(".dropdown-help .dropdown-menu").on("click", "input, button", function (e) { | |||
e.stopPropagation(); | |||
}); | |||
$(document).trigger('toolbar_setup'); | |||
$("#input-help").on("keydown", function (e) { | |||
if(e.which == 13) { | |||
var keywords = $(this).val(); | |||
show_help_results(keywords); | |||
$(this).val(""); | |||
} | |||
}); | |||
// clear all custom menus on page change | |||
$(document).on("page-change", function() { | |||
$("header .navbar .custom-menu").remove(); | |||
$("#input-help + span").on("click", function () { | |||
var keywords = $(this).val(); | |||
show_help_results(keywords); | |||
$(this).val(""); | |||
}); | |||
frappe.search.setup(); | |||
}, | |||
$(document).on("page-change", function () { | |||
var $help_links = $(".dropdown-help #help-links"); | |||
$help_links.html(""); | |||
var route = frappe.get_route_str(); | |||
var breadcrumbs = route.split("/"); | |||
var links = []; | |||
for (var i = 0; i < breadcrumbs.length; i++) { | |||
var r = route.split("/", i + 1); | |||
var key = r.join("/"); | |||
var help_links = frappe.help.help_links[key] || []; | |||
links = $.merge(links, help_links); | |||
} | |||
if(links.length === 0) { | |||
$help_links.next().hide(); | |||
} | |||
else { | |||
$help_links.next().show(); | |||
} | |||
for (var i = 0; i < links.length; i++) { | |||
var link = links[i]; | |||
var url = link.url; | |||
var data_path = url.slice(url.indexOf('/user')); | |||
if(data_path.lastIndexOf('.')){ | |||
data_path = data_path.slice(0, data_path.lastIndexOf('.')); | |||
} | |||
$("<a>", { | |||
href: link.url, | |||
text: link.label, | |||
target: "_blank", | |||
"data-path": data_path | |||
}).appendTo($help_links); | |||
} | |||
$('.dropdown-help .dropdown-menu').on('click', 'a', show_results); | |||
}); | |||
var $help_modal = frappe.get_modal("Help", ""); | |||
$help_modal.addClass('help-modal'); | |||
var $result_modal = frappe.get_modal("", ""); | |||
$result_modal.addClass("help-modal"); | |||
$(document).on("click", ".help-modal a", show_results); | |||
function show_help_results(keywords) { | |||
frappe.call({ | |||
method: "frappe.utils.help.get_help", | |||
args: { | |||
text: keywords | |||
}, | |||
callback: function (r) { | |||
var results = r.message || []; | |||
var result_html = "<h4 style='margin-bottom: 25px'>Showing results for '" + keywords + "' </h4>"; | |||
for (var i = 0, l = results.length; i < l; i++) { | |||
var title = results[i][0]; | |||
var intro = results[i][1]; | |||
var fpath = results[i][2]; | |||
result_html += "<div class='search-result'>" + | |||
"<a href='#' class='h4' data-path='"+fpath+"'>" + title + "</a>" + | |||
"<p>" + intro + "</p>" + | |||
"</div>"; | |||
} | |||
if(results.length === 0) { | |||
result_html += "<p class='padding'>No results found</p>"; | |||
} | |||
$help_modal.find('.modal-body').html(result_html); | |||
$help_modal.modal('show'); | |||
} | |||
}); | |||
} | |||
function show_results(e) { | |||
//edit links | |||
href = e.target.href; | |||
if(href.indexOf('blob') > 0) { | |||
window.open(href, '_blank'); | |||
} | |||
var converter = new Showdown.converter(); | |||
var path = $(this).attr("data-path"); | |||
if(path) { | |||
e.preventDefault(); | |||
frappe.call({ | |||
method: "frappe.utils.help.get_help_content", | |||
args: { | |||
path: path | |||
}, | |||
callback: function (r) { | |||
if(r.message){ | |||
var title = r.message[0][0]; | |||
var content = r.message[0][1]; | |||
$result_modal.find('.modal-title').html("<span>" + title + "</span>"); | |||
$result_modal.find('.modal-body').html(content); | |||
$result_modal.modal('show'); | |||
} | |||
} | |||
}); | |||
} | |||
} | |||
} | |||
}); | |||
$.extend(frappe.ui.toolbar, { | |||
@@ -117,6 +117,14 @@ kbd { | |||
// only rounded bottoms | |||
border-radius: 0px 0px 4px 4px; | |||
} | |||
.dropdown-menu .dropdown-header { | |||
padding: 3px 14px; | |||
font-size: 11px; | |||
font-weight: 200; | |||
padding-top: 12px; | |||
} | |||
.dropdown-menu .divider { | |||
@@ -449,3 +449,56 @@ textarea.form-control { | |||
margin: 15px 0px; | |||
} | |||
} | |||
.screenshot { | |||
border: 1px solid @border-color; | |||
box-shadow: 1px 1px 7px rgba(0,0,0,0.15); | |||
margin: 8px 0px; | |||
max-width: 100%; | |||
} | |||
.help-modal { | |||
a { | |||
color: @brand-primary; | |||
} | |||
.modal-dialog { | |||
width: 768px; | |||
} | |||
.modal-body { | |||
padding: 15px 27px; | |||
} | |||
.parent-link { | |||
line-height: 4em; | |||
&:before { | |||
font-family: 'Octicons'; | |||
content: '\f0a4'; | |||
} | |||
} | |||
.edit-container { | |||
padding-bottom: 12px; | |||
} | |||
@media (max-width: @screen-xs) { | |||
.modal-dialog { | |||
width: auto; | |||
} | |||
.modal-content { | |||
height: auto !important; | |||
} | |||
iframe { | |||
height: auto; | |||
width: 100%; | |||
} | |||
} | |||
} | |||
.search-result { | |||
margin-bottom: 24px; | |||
} |
@@ -350,13 +350,6 @@ p { | |||
border-radius: 15px; | |||
} | |||
.screenshot { | |||
border: 1px solid @border-color; | |||
box-shadow: 1px 1px 7px rgba(0,0,0,0.15); | |||
margin: 8px 0px; | |||
max-width: 100%; | |||
} | |||
hr { | |||
margin: 48px 0px 30px; | |||
} | |||
@@ -37,7 +37,7 @@ | |||
@media (max-width: 991px) { | |||
.navbar-desk { | |||
width: 40% !important; | |||
width: 35% !important; | |||
& ~ ul > li { | |||
float: left; | |||
@@ -59,7 +59,7 @@ | |||
@media (max-width: 767px) { | |||
.navbar-desk { | |||
width: 60% !important; | |||
width: 50% !important; | |||
} | |||
} | |||
@@ -95,9 +95,43 @@ | |||
} | |||
} | |||
.dropdown-help .dropdown-menu { | |||
width: 350px !important; | |||
max-height: 440px; | |||
overflow: scroll; | |||
.input-group { | |||
width: 100%; | |||
background-color: #f5f7fa; | |||
padding: 8px 12px; | |||
} | |||
input { | |||
width: 100%; | |||
padding: 5px 10px; | |||
outline: none; | |||
border-radius: 3px 0 0 3px; | |||
border: 1px solid #d1d8dd; | |||
opacity: 0.9; | |||
line-height: 1.5; | |||
} | |||
button { | |||
border: 1px solid #d1d8dd; | |||
&.loading { | |||
} | |||
} | |||
@media (max-width: @screen-xs) { | |||
position: fixed !important; | |||
top: 40px; | |||
width: 100% !important; | |||
} | |||
} | |||
@media (max-width: 767px) { | |||
.dropdown-navbar-new-comments.open .dropdown-menu, | |||
.dropdown-navbar-user.open .dropdown-menu { | |||
.dropdown-mobile.open .dropdown-menu { | |||
position: absolute; | |||
border-top: 1px solid rgba(0, 0, 0, 0.14902); | |||
box-shadow: 0 6px 12px rgba(0, 0, 0, .175); | |||
@@ -128,6 +162,10 @@ | |||
#navbar-search { | |||
width: 300px; | |||
background-color: rgba(255, 255, 255, 0.9); | |||
@media (max-width: @screen-sm) { | |||
width: 250px; | |||
} | |||
} | |||
.navbar .navbar-search-icon { | |||
@@ -2,14 +2,10 @@ | |||
<nav class="navbar navbar-default navbar-main" role="navigation"> | |||
<div class="container"> | |||
<div class="navbar-header"> | |||
<!-- <a class="navbar-brand ellipsis" href="{{ url_prefix }}{{ home_page or "/" }}"> --> | |||
<a class="navbar-brand ellipsis" | |||
href="{{ url_prefix }}{{ home_page or "/"}}"> | |||
<span>{{ brand_html or (frappe.get_hooks("brand_html") or ["Home"])[0] }}</span> | |||
</a> | |||
<!-- <a class="pull-right visible-xs navbar-toggle toggle-sidebar"> | |||
<i class="octicon octicon-three-bars"></i> | |||
</a> --> | |||
<div class="dropdown"> | |||
<button class="btn btn-default navbar-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> | |||
<i class="octicon octicon-three-bars"></i> | |||
@@ -9,6 +9,9 @@ from frappe.model.db_schema import DbManager | |||
from frappe.installer import get_root_connection | |||
from frappe.database import Database | |||
import os | |||
import re | |||
from markdown2 import markdown | |||
from bs4 import BeautifulSoup | |||
help_db_name = '_frappe_help' | |||
@@ -22,6 +25,10 @@ def sync(): | |||
def get_help(text): | |||
return HelpDatabase().search(text) | |||
@frappe.whitelist() | |||
def get_help_content(path): | |||
return HelpDatabase().get_content(path) | |||
class HelpDatabase(object): | |||
def __init__(self): | |||
self.make_database() | |||
@@ -42,13 +49,23 @@ class HelpDatabase(object): | |||
def make_table(self): | |||
if not 'help' in self.db.get_tables(): | |||
self.db.sql('''create table help(path text, content text, fulltext(content)) | |||
self.db.sql('''create table help(path text, content text, title text, intro text, fulltext(title), fulltext(content)) | |||
COLLATE=utf8mb4_unicode_ci | |||
ENGINE=MyISAM | |||
CHARACTER SET=utf8mb4''') | |||
def search(self, words): | |||
return self.db.sql('select path, content from help where match(content) against (%s) limit 10', words) | |||
return self.db.sql('select title, intro, path from help where match(content) against (%s) limit 10', words) | |||
def get_content(self, path): | |||
query = 'select title, content from help where path like "%{path}%" order by path desc' | |||
path2 = path | |||
if not path2.endswith('index'): | |||
path2 += "index" if path2.endswith('/') else "/index" | |||
result = self.db.sql(query.format(path=path2)) | |||
if not result: | |||
result = self.db.sql(query.format(path=path)) | |||
return result | |||
def sync_pages(self): | |||
self.db.sql('truncate help') | |||
@@ -56,11 +73,116 @@ class HelpDatabase(object): | |||
docs_folder = '../apps/{app}/{app}/docs/user'.format(app=app) | |||
if os.path.exists(docs_folder): | |||
for basepath, folders, files in os.walk(docs_folder): | |||
files = self.reorder_files(files) | |||
for fname in files: | |||
if fname.rsplit('.', 1)[-1] in ('md', 'html'): | |||
fpath = os.path.join(basepath, fname) | |||
with open(fpath, 'r') as f: | |||
content = frappe.render_template(unicode(f.read(), 'utf-8'), | |||
{'docs_base_url': '/assets/{app}_docs'.format(app=app)}) | |||
content = self.make_content(content, fpath) | |||
title = self.make_title(basepath, fname, content) | |||
intro = self.make_intro(content) | |||
#relpath = os.path.relpath(fpath, '../apps/{app}'.format(app=app)) | |||
self.db.sql('''insert into help(path, content) | |||
values (%s, %s)''', (fpath, f.read())) | |||
self.db.sql('''insert into help(path, content, title, intro) | |||
values (%s, %s, %s, %s)''', (fpath, content, title, intro)) | |||
def make_title(self, basepath, filename, html): | |||
if '<h1>' in html: | |||
title = html.split("<h1>", 1)[1].split("</h1>", 1)[0] | |||
elif 'index' in filename: | |||
title = basepath.rsplit('/', 1)[-1].title().replace("-", " ") | |||
else: | |||
title = filename.rsplit('.', 1)[0].title().replace("-", " ") | |||
return title | |||
def make_intro(self, html): | |||
intro = "" | |||
if '<p>' in html: | |||
intro = html.split('<p>', 1)[1].split('</p>', 1)[0] | |||
if 'Duration' in html: | |||
intro = "Help Video: " + intro | |||
return intro | |||
def make_content(self, content, path): | |||
html = markdown(content) | |||
if '{index}' in html: | |||
path = path.rsplit("/", 1)[0] | |||
index_path = os.path.join(path, "index.txt") | |||
if os.path.exists(index_path): | |||
with open(index_path, 'r') as f: | |||
lines = f.read().split('\n') | |||
links_html = "<ol class='index-links'>" | |||
for line in lines: | |||
fpath = os.path.join(path, line) | |||
title = line.title().replace("-", " ") | |||
if title: | |||
links_html += "<li><a data-path='{fpath}'> {title} </a></li>".format(fpath=fpath, title=title) | |||
links_html += "</ol>" | |||
html = html.replace('{index}', links_html) | |||
if '{next}' in html: | |||
html = html.replace('{next}', '') | |||
target = path.split('/', 3)[-1] | |||
app_name = path.split('/', 3)[2] | |||
html += ''' | |||
<div class="page-container"> | |||
<div class="page-content"> | |||
<div class="edit-container text-center"> | |||
<i class="icon icon-smile"></i> | |||
<a class="text-muted edit" href="https://github.com/frappe/{app_name}/blob/develop/{target}"> | |||
Improve this page | |||
</a> | |||
</div> | |||
</div> | |||
</div>'''.format(app_name=app_name, target=target) | |||
soup = BeautifulSoup(html, 'html.parser') | |||
for link in soup.find_all('a'): | |||
if link.has_attr('href'): | |||
url = link['href'] | |||
if '/user' in url: | |||
data_path = url[url.index('/user'):] | |||
if '.' in data_path: | |||
data_path = data_path[: data_path.rindex('.')] | |||
if data_path: | |||
link['data-path'] = data_path | |||
parent = self.get_parent(path) | |||
if parent: | |||
parent_tag = soup.new_tag('a') | |||
parent_tag.string = parent['title'] | |||
parent_tag['class'] = 'parent-link' | |||
parent_tag['data-path'] = parent['path'] | |||
soup.find().insert_before(parent_tag) | |||
return soup.prettify() | |||
def get_parent(self, child_path): | |||
path = child_path | |||
if 'index' in child_path: | |||
child_path = child_path[: child_path.rindex('index')] | |||
if child_path[-1] == '/': | |||
child_path = child_path[:-1] | |||
parent_path = child_path[: child_path.rindex('/')] + "/index" | |||
result = self.get_content(parent_path) | |||
if result: | |||
title = result[0][0] | |||
return { 'title': title, 'path': parent_path } | |||
else: | |||
return None | |||
def reorder_files(self, files): | |||
pos = 0 | |||
if 'index.md' in files: | |||
pos = files.index('index.md') | |||
elif 'index.html' in files: | |||
pos = files.index('index.html') | |||
if pos: | |||
files[0], files[pos] = files[pos], files[0] | |||
return files |