Skip to content

Commit

Permalink
Merge branch 'release-2.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
abidibo committed Feb 22, 2021
2 parents 5dec20b + 1e0f84a commit 8def866
Show file tree
Hide file tree
Showing 20 changed files with 318 additions and 11 deletions.
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Login with user `demo` and password `demo`
- [Installation](#installation)
- [Configuration](#configuration)
- [Menu](#configuration-menu)
- [Search Field](#configuration-search-field)
- [Analytics](#configuration-analytics)
- [Signals](#signals)
- [Js Utilities](#js-utilities)
Expand All @@ -53,6 +54,7 @@ Everything is styled through CSS and when required, JS is used.
- Based on Bootstrap 5 and FontAwesome Free 5
- Fully responsive
- Custom and flexible sidebar menu
- Configurable search field
- Text input filters and dropdown list filters facilities
- Form tabs out of the box
- Easy way to include templates in the change form and change list pages
Expand Down Expand Up @@ -153,6 +155,10 @@ BATON = {
'MESSAGES_TOASTS': False,
'GRAVATAR_DEFAULT_IMG': 'retro',
'LOGIN_SPLASH': '/static/core/img/login-splash.png',
'SEARCH_FIELD': {
'label': 'Search contents...',
'url': '/search/',
},
'MENU': (
{ 'type': 'title', 'label': 'main', 'apps': ('auth', ) },
{
Expand Down Expand Up @@ -203,7 +209,7 @@ Default value is `True`.
- `GRAVATAR_DEFAULT_IMG`: the default gravatar image displayed if the user email is not associated to any gravatar image. Possible values: 404, mp, identicon, monsterid, wavatar, retro, robohash, blank (see [http://en.gravatar.com/site/implement/images/](http://en.gravatar.com/site/implement/images/)).
- `LOGIN_SPLASH`: an image used as body background in the login page. The image is centered and covers the whole viewport.

`MENU` and `ANALYTICS` configurations in detail:
`MENU`, `SEARCH_FIELD` and `ANALYTICS` configurations in detail:

### <a name="configuration-menu"></a>MENU

Expand Down Expand Up @@ -248,6 +254,57 @@ You can specify free voices. You must define a _url_ and if you want some visibi
're': '^/admin/news/category/(\d*)?'
}

### <a name="configuration-search-field"></a>SEARCH FIELD

With Baton you can optionally configure a search field in the sidebar above the menu.

![Search field](docs/images/search-field.png)

This is an autocomplete field, which will calls a custom api at every keyup event (for strings of length > 3). Such api receives the `text` param in the querystring and should return a json response including the search results in the form:

```
{
length: 2,
data: [
{ label: 'My result #1', icon: 'fa fa-edit', url: '/admin/myapp/mymodel/1/change' },
// ...
]
}
```

You should provide the results length and the data as an array of objects which must contain the `label` and `url` keys. The `icon` key is optional.

Let's see an example:

```
@staff_member_required
def admin_search(request):
text = request.GET.get('text', None)
res = []
news = News.objects.all()
if text:
news = news.filter(title__icontains=text)
for n in news:
res.append({
'label': str(n) + ' edit',
'url': '/admin/news/news/%d/change' % n.id,
'icon': 'fa fa-edit',
})
if text.lower() in 'Lucio Dalla Wikipedia'.lower():
res.append({
'label': 'Lucio Dalla Wikipedia',
'url': 'https://www.google.com',
'icon': 'fab fa-wikipedia-w'
})
return JsonResponse({
'length': len(res),
'data': res
})
```

You can move between the results using the keyboard up and down arrows, and you can browse to the voice url pressing Enter.


### <a name="configuration-analytics"></a>ANALYTICS

You can create a cool index page displaying some statistics widgets using the Google Analytics API just by defining the `ANALYTICS` setting.
Expand Down Expand Up @@ -380,6 +437,7 @@ Baton.translations = {
filter: 'Filter',
close: 'Close',
save: 'Save',
search: 'Search',
cannotCopyToClipboardMessage: 'Cannot copy to clipboard, please do it manually: Ctrl+C, Enter',
retrieveDataError: 'There was an error retrieving the data'
}
Expand Down
1 change: 1 addition & 0 deletions baton/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
'MESSAGES_TOASTS': False,
'GRAVATAR_DEFAULT_IMG': 'retro',
'LOGIN_SPLASH': None,
'SEARCH_FIELD': None,
}


Expand Down
8 changes: 4 additions & 4 deletions baton/static/baton/app/dist/baton.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion baton/static/baton/app/dist/baton.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion baton/static/baton/app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion baton/static/baton/app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "baton",
"version": "2.0.1",
"version": "2.1.0",
"description": "Django Baton App",
"main": "index.js",
"scripts": {
Expand Down
94 changes: 94 additions & 0 deletions baton/static/baton/app/src/core/Menu.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import $ from 'jquery'
import Translator from './i18n'

let Menu = {
/**
Expand All @@ -8,15 +9,18 @@ let Menu = {
*/
init: function (config, Dispatcher) {
this.Dispatcher = Dispatcher
this.t = new Translator($('html').attr('lang'))
this.collapsableUserArea = config.collapsableUserArea
this.menuTitle = config.menuTitle
this.searchField = config.searchField
this.appListUrl = config.api.app_list
this.gravatarUrl = config.api.gravatar
this.gravatarDefaultImg = config.gravatarDefaultImg
this.alwaysCollapsed = $('#header').hasClass('menu-always-collapsed')
this.fixNodes()
this.brandingClone = $('#branding').clone()
this.manageBrandingUserTools()
this.manageSearchField()
this.fetchData()
this.setHeight()
let self = this
Expand Down Expand Up @@ -81,6 +85,96 @@ let Menu = {
}
}
},
manageSearchField () {
// unset
if (!this.searchField || !this.searchField.url) {
return
}

let container = $('<div />', { class: 'search-field-tool' })

let field = $('<input />', {
class: 'form-control form-control-sm',
type: 'text',
list: 'admin-search-datalist',
placeholder: this.searchField.label || this.t('search')
})
let dataList = $('<div />', { id: 'admin-search-datalist' }).on('mouseover', e => {
if ($(e.target).hasClass('datalist-option') || $(e.target).parent('.datalist-option').length) {
dataList.find('.datalist-option').removeClass('selected')
let item = $(e.target).hasClass('datalist-option') ? $(e.target) : $(e.target).parent('.datalist-option')
item.addClass('selected')
}
})

let navigateDataList = code => {
let target
let active = dataList.find('.datalist-option.selected').first()
if (!active.length) {
target = dataList.find('.datalist-option')[code === 40 ? 'first' : 'last']()
} else {
if (code === 40) {
let next = active.next()
target = next.length ? next : dataList.find('.datalist-option').first()
} else {
let prev = active.prev()
target = prev.length ? prev : dataList.find('.datalist-option').last()
}
}
if (target) {
active.removeClass('selected')
$(target).addClass('selected')
target[0].scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'nearest'
})
}
}

field.on('blur', () => dataList.empty())
field.on('focus', () => field.trigger('keyup', { keyCode: 39 }))
field.on('keyup', e => {
var code = e.keyCode || e.which

if (code === 13) {
// goto url if there is an active voice
let active = dataList.find('.datalist-option.selected').first()
if (active.length) {
location.href = active.attr('data-url')
}
return
}

if ([40, 38].indexOf(code) !== -1) {
// move
navigateDataList(code)
} else {
// search
if ($(field).val().length <= 3) {
dataList.empty()
return
}

container.addClass('loading')
$.getJSON(this.searchField.url, { text: $(field).val() })
.done(data => {
container.removeClass('loading')
dataList.empty()
data.data.forEach((r, index) => dataList.append(`
<div class="datalist-option${index === 0 ? ' selected' : ''}" data-url="${r.url}"><span>${r.label}</span>${r.icon ? '<i class="' + r.icon + '"></i>' : ''}</div>`)
)
})
.fail((jqxhr, textStatus, err) => {
console.log(err)
container.removeClass('loading')
dataList.empty()
})
}
})

$('#user-tools-sidebar').after(container.append([field, dataList]))
},
renderUserTools: function () {
let self = this
let container = $('<div />', { id: 'user-tools-sidebar' })
Expand Down
4 changes: 4 additions & 0 deletions baton/static/baton/app/src/core/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export const messages = {
en: 'Save',
it: 'Salva'
},
search: {
en: 'Search',
it: 'Cerca'
},
cannotCopyToClipboardMessage: {
en: 'Cannot copy to clipboard, please do it manually: Ctrl+C, Enter',
it: 'Impossibile copiare negli appunti, copiare manualmente: Ctrl+C, Enter'
Expand Down
58 changes: 58 additions & 0 deletions baton/static/baton/app/src/styles/_menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,64 @@ body:not(.menu-mobile) {
}
}

.search-field-tool {
margin: .5rem 1rem;
position: relative;

input {
background: $search-field-input-bg;
border-color: $search-field-input-border-color;
color: $search-field-color;
padding-right: 30px;
}

&::after {
color: $secondary;
content: '\f002';
font-family: 'Font Awesome\ 5 Free';
position: absolute;
right: .5rem;
top: .2rem;
}

&.loading {
&::after {
animation: fa-spin 2s linear infinite;
content: '\f1ce';
}
}

#admin-search-datalist {
background: $search-field-datalist-bg;
max-height: 50vh;
overflow: auto;
position: absolute;
width: 100%;

div {
align-items: center;
border-bottom: 1px solid darken($search-field-datalist-bg, 2%);
cursor: pointer;
display: flex;
flex-direction: row;
font-size: .9rem;
justify-content: space-between;
max-height: 50vh;
overflow: auto;
padding: .5rem;

&.selected {
background: lighten($search-field-datalist-bg, 5%);
}

i {
color: $search-field-icon-color;
font-size: .9rem;
}
}
}
}

h1 {
@extend .clearfix;
background: $menu-mobile-title-bg;
Expand Down
7 changes: 7 additions & 0 deletions baton/static/baton/app/src/styles/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ $menu-voice-title-bg: darken($menu-bg, 5%);
$menu-active-bg: #f5f5f5;
$menu-branding-bg: darken($menu-bg, 5%);

// search field
$search-field-input-bg: lighten($menu-bg, 10%);
$search-field-input-border-color: $menu-bg;
$search-field-color: $menu-color;
$search-field-datalist-bg: darken($menu-bg, 5%);
$search-field-icon-color: lighten(#007eed, 25%);

// dashboard
$dashboard-bg: #f5f5f5;
$dashboard-title-bg: #fafafa;
Expand Down
1 change: 1 addition & 0 deletions baton/templatetags/baton_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def baton_config():
"messagesToasts": get_config('MESSAGES_TOASTS'),
"gravatarDefaultImg": get_config('GRAVATAR_DEFAULT_IMG'),
"loginSplash": get_config('LOGIN_SPLASH'),
"searchField": get_config('SEARCH_FIELD'),
}

return conf
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@
# built documents.
#
# The short X.Y version.
version = u'2.0.1'
version = u'2.1.0'
# The full version, including alpha/beta/rc tags.
release = u'2.0.1'
release = u'2.1.0'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down

0 comments on commit 8def866

Please sign in to comment.