wcs: add support for custom content layout (#52073)

This commit is contained in:
Thomas Jund 2021-05-05 14:52:49 +02:00 committed by Frédéric Péters
parent 9761e251c5
commit 93ea996af9
8 changed files with 500 additions and 13 deletions

View File

@ -0,0 +1,9 @@
{% spaceless %}
{% if field.type == "date" %}
{{ value|date }}
{% elif field.type == "bool" and value is not None %}
{{ value|yesno }}
{% else %}
{{ value|default:"" }}
{% endif %}
{% endspaceless %}

View File

@ -9,25 +9,51 @@
{% endblock %}
{% if card %}
<div>
<div class="cell--body">
{% spaceless %}
{% if cell.custom_schema %}
<div class="{{ cell.custom_schema.grid_class }}">
{% for item in cell.custom_schema.cells %}
<div class="{{ item.cell_size|default:"" }}">
{% for field in schema.fields %}
{% if field.varname == item.varname %}
{% with card.fields|get:item.varname as value %}
{% if item.display_mode == "title" %}
<h3>{% include "combo/wcs/card-field-value.html" %}</h3>
{% endif %}
{% if item.display_mode == "label" or item.display_mode == "label-and-value" %}
<p class="label">{{ field.label }}</p>
{% endif %}
{% if item.display_mode == "value" or item.display_mode == "label-and-value" %}
<p class="value">
{% include "combo/wcs/card-field-value.html" %}
</p>
{% endif %}
{% endwith %}
{% endif %}
{% endfor%}
</div>
{% endfor%}
</div>
{% else %}
{% for field in schema.fields %}
{% if 'varname' in field and field.varname and field.type != 'file' %}
{% with card.fields|get:field.varname as value %}
<p>
<span class="label">{{ field.label }}</span>
{% if field.type == "date" %}
<span class="value">{{ value|date }}</span>
{% elif field.type == "bool" and value is not None %}
<span class="value">{{ value|yesno }}</span>
{% else %}
<span class="value">{{ value|default:"" }}</span>
{% endif %}
<span class="value">{% include "combo/wcs/card-field-value.html" %}</span>
</p>
{% endwith %}
{% endif %}
{% endfor %}
{% endif %}
{% endspaceless %}
{% else %}
<p>{% trans 'Unknown Card' %}</p>
<div class="cell--body">
<p>{% trans 'Unknown Card' %}</p>
</div>
{% endif %}
</div>
</div>

View File

@ -1,8 +1,82 @@
{% extends "combo/cell_form.html" %}
{% load i18n %}
{% block cell-form %}
{{ block.super }}
{% if card_schema %}
{{ card_schema|json_script:card_schema_id }}
{# UI to customize content layout #}
<div class="wcs-cards-cell--grid">
<div class="wcs-cards-cell--grid-options">
<span class="wcs-cards-cell--grid-layout-label">{% trans "Automatic" %}</span>
<a role="button" class="wcs-cards-cell--grid-layout-btn">
{% trans "Edit" %}
</a>
</div>
<div class="wcs-cards-cell--grid-cells">
</div>
<div class="wcs-cards-cell--grid-buttons">
<button type="button" class="wcs-cards-cell--add-grid-cell-btn">{% trans "Add" %}</button>
</div>
</div>
{# templates for JS #}
<script type="text/template" class="wcs-cards-cell--grid-form-tpl">
<form>
<p>
{% trans "Layout" %}
<select name="grid-layout">
<option value="fx-grid--auto">{% trans "Automatic" %}</option>
<option value="fx-grid">{% trans "1 column" %}</option>
<option value="fx-grid--t2">{% trans "2 columns" %}</option>
<option value="fx-grid--t3">{% trans "3 columns" %}</option>
</select>
</p>
</form>
</script>
<script type="text/template" class="wcs-cards-cell--grid-cell-form-tpl">
<form>
<p>
<label>
{% trans "Field" %}
<select name="card_field_varname"></select>
</label>
</p>
<p>
<label>
{% trans "Display" %}
<select name="display_mode">
<option value="label-and-value">{% trans "Label & Value" %}</option>
<option value="label">{% trans "Label only" %}</option>
<option value="value">{% trans "Value only" %}</option>
<option value="title">{% trans "Title" %}</option>
</select>
</label>
</p>
<p>
<label>
{% trans "Size" %}
<select name="cell_size">
<option value="">{% trans "Automatic" %}</option>
<option value="size--1-1">1/1</option>
<option value="size--t1-2">1/2</option>
<option value="size--t1-3">1/3</option>
<option value="size--t2-3">2/3</option>
</select>
</label>
</p>
</form>
</script>
<script type="text/template" class="wcs-cards-cell--grid-cell-tpl">
<div class="wcs-cards-cell--grid-cell">
<div class="wcs-cards-cell--grid-cell-content"></div>
<div class="wcs-cards-cell--grid-cell-buttons">
<a role="button" class="wcs-cards-cell--grid-cell-edit">{% trans "Edit" %}</a>
<a role="button" class="wcs-cards-cell--grid-cell-delete">{% trans "Delete" %}</a>
</div>
</div>
</script>
{% endif %}
{% endblock %}

View File

@ -602,3 +602,77 @@ form .choices {
margin: 0 0 0 1em;
padding: 0;
}
.wcs-cards-cell--grid {
border : 1px solid #aaa;
/*resize: horizontal;*/
overflow: hidden;
}
.wcs-cards-cell--grid-options {
background-color: #eee;
padding: 3px;
}
.wcs-cards-cell--grid-cells[class*="fx-grid--"],
.wcs-cards-cell--grid-buttons {
padding: 3px;
}
.wcs-cards-cell--grid-cell {
box-sizing: border-box;
border: 1px solid #aaa;
padding: 3px;
margin-top: 3px;
margin-bottom: 3px;
background-color: #fff;
cursor: move;
display: flex;
flex-direction: column;
}
.wcs-cards-cell--grid-cell.ui-sortable-helper {
opacity: 0.7;
}
.wcs-cards-cell--grid-cell-placeholder {
background-color: #eee;
}
.wcs-cards-cell--grid-cell-content {
flex-grow: 1;
}
.wcs-cards-cell--grid-cell-content > * {
display: block;
}
.wcs-cards-cell--grid-cell-content > .key {
font-weight: bold;
}
.wcs-cards-cell--grid-cell-content > .title {
font-size: 1.2em;
font-weight: bold;
}
.wcs-cards-cell--grid-layout-label,
.wcs-cards-cell--grid-cell-content .cell-size-label {
color: #666;
}
.wcs-cards-cell--grid-layout-label::before,
.wcs-cards-cell--grid-cell-content .cell-size-label::before {
content: " [ ";
}
.wcs-cards-cell--grid-layout-label::after,
.wcs-cards-cell--grid-cell-content .cell-size-label::after {
content: " ]";
}
.wcs-cards-cell--grid-cell-buttons {
cursor: default;
display: flex;
justify-content: space-between;
}

View File

@ -398,3 +398,304 @@ $(function() {
return false;
});
});
// UI to customize the layout of the content of a wcs-card-infos-cell
const Card_cell_custom = function(cell) {
this.cell = cell;
this.gridSchema_default = {
"grid_class": "fx-grid--auto",
"grid_layout_label": gettext('Automatic'),
"cells": []
}
this.init();
}
Card_cell_custom.prototype = {
parse_string_tpl: function(string_tpl) {
const wrapper = document.createElement('div');
wrapper.innerHTML = string_tpl;
return wrapper.firstElementChild;
},
field_with_varname: function(varname) {
const field = this.cardSchema.fields.filter(function(i) {
return i.varname === varname;
})
return field[0];
},
// Grid methods
grid__form_dialog: function(callback){
const _self = this;
$(this.grid_form).dialog({
modal: true,
'title': gettext('Grid Layout'),
width: 'auto',
buttons: [
{
text: gettext('Cancel'),
class:"cancel-button",
click: function() {
$(this).dialog("close");
}
},{
text: gettext('Edit'),
class: "submit-button",
click: function() {
const form_datas = {};
const select_layout = _self.grid_form[0];
form_datas.grid_class = select_layout.value;
form_datas.label = select_layout[select_layout.selectedIndex].text;
callback(form_datas);
$(this).dialog("close");
}
}
]
});
},
grid__set_schema: function(form_datas){
this.gridSchema.grid_class = form_datas.grid_class;
this.gridSchema.grid_layout_label = form_datas.label;
this.grid__set_layout();
$(this.cell).trigger("custom_cell:change");
},
grid__set_layout: function() {
this.grid_layout_label.textContent = this.gridSchema.grid_layout_label;
if (this.grid_wrapper.dataset.grid_layout) {
this.grid_wrapper.classList.remove(this.grid_wrapper.dataset.grid_layout); }
this.grid_wrapper.classList.add(this.gridSchema.grid_class);
this.grid_wrapper.dataset.grid_layout = this.gridSchema.grid_class;
},
grid_toggle: function() {
if (this.toggleBtn.checked) {
this.grid.hidden = false;
} else {
this.grid.hidden = true;
}
},
// Grid cell methods
grid_cell__init_form: function() {
this.grid_cell_form.varname = this.grid_cell_form[0];
this.grid_cell_form.display_mode = this.grid_cell_form[1];
this.grid_cell_form.size = this.grid_cell_form[2];
const varname_select = this.grid_cell_form.varname;
this.cardSchema.fields.forEach(function(el, id) {
if (el.varname) {
$('<option />')
.attr('value', el.varname)
.text(el.label)
.appendTo(varname_select);
}
})
},
grid_cell__form_dialog: function(button_label, callback){
const _self = this;
$(this.grid_cell_form).dialog({
modal: true,
'title': button_label,
width: 'auto',
buttons: [
{
text: gettext('Cancel'),
class:"cancel-button",
click: function() {
$(this).dialog("close");
}
},{
text: button_label,
class: "submit-button",
click: function() {
const form_datas = {};
$(_self.grid_cell_form).serializeArray().forEach(function(el){
form_datas[el.name] = el.value;
});
callback(form_datas);
$(this).dialog("close");
}
}
]
});
},
grid_cell__new: function() {
const el = this.grid_cell_tpl.cloneNode(true);
el.deletBtn = el.querySelector('.wcs-cards-cell--grid-cell-delete');
el.editBtn = el.querySelector('.wcs-cards-cell--grid-cell-edit');
el.contentEl = el.querySelector('.wcs-cards-cell--grid-cell-content');
return el;
},
grid_cell__set: function(grid_cell, schema_cell) {
const _self = this;
// Set size css class
if (!grid_cell.classList.contains(schema_cell.cell_size)) {
if (grid_cell.dataset.cell_size) grid_cell.classList.remove(grid_cell.dataset.cell_size);
if (schema_cell.cell_size) grid_cell.classList.add(schema_cell.cell_size);
}
// Store Schema in el data
for (const key in schema_cell) {
grid_cell.dataset[key] = schema_cell[key];
}
// set cell text
let cell_text = "";
cell_text += '<span class="' + schema_cell.display_mode + '">' + _self.field_with_varname(schema_cell.varname).label + '</span>';
cell_text += '<span class="cell-meta">';
let cell_display_mode_label = $(this.grid_cell_form).find('select[name="display_mode"] option[value="' + schema_cell.display_mode + '"]').text();
cell_text += '<span class="cell-display-mode-label">' + cell_display_mode_label + '</span>';
let cell_size_label = $(this.grid_cell_form).find('select[name="cell_size"] option[value="' + schema_cell.cell_size + '"]').text();
cell_text += '<span class="cell-size-label">' + cell_size_label + '</span>';
cell_text += '</span>';
grid_cell.contentEl.innerHTML = cell_text;
},
grid_cell__add: function(schema_cell) {
const _self = this;
const new_grid_cell = this.grid_cell__new();
this.grid_cell__set(new_grid_cell, schema_cell);
new_grid_cell.deletBtn.addEventListener('click', function() {_self.grid_cell__delete(new_grid_cell)});
new_grid_cell.editBtn.addEventListener('click', function() {_self.grid_cell__edit(new_grid_cell)});
this.grid_wrapper.append(new_grid_cell);
},
grid_cell__delete: function(grid_cell) {
const cell_id = $(grid_cell).index();
this.gridSchema.cells = this.gridSchema.cells.filter(function(el, i){
return i !== cell_id;
});
$(grid_cell).remove();
$(this.cell).trigger("custom_cell:change");
},
grid_cell__edit: function(grid_cell) {
const _self = this;
this.grid_cell_form.varname.value = grid_cell.dataset.varname;
this.grid_cell_form.size.value = grid_cell.dataset.cell_size;
this.grid_cell_form.display_mode.value = grid_cell.dataset.display_mode;
const cell_id = $(grid_cell).index();
const grid_cell_schema = this.gridSchema.cells[cell_id];
this.grid_cell__form_dialog(gettext('Edit'), function(form_datas){
const grid_cell_schema_mod = _self.grid_cell__set_schema(form_datas, grid_cell_schema);
_self.grid_cell__set(grid_cell, grid_cell_schema_mod);
$(_self.cell).trigger("custom_cell:change");
});
},
grid_cell__set_schema: function(form_datas, schema_cell) {
schema_cell.varname = form_datas.card_field_varname;
schema_cell.cell_size = form_datas.cell_size;
schema_cell.display_mode = form_datas.display_mode;
return schema_cell
},
grid_cell__add_schema: function(form_datas) {
let schema_cell = {};
schema_cell = this.grid_cell__set_schema(form_datas, schema_cell);
this.gridSchema.cells.push(schema_cell);
this.grid_cell__add(schema_cell);
$(this.cell).trigger("custom_cell:change");
compute_max_height($(this.cell));
},
grid_cell__init: function() {
if (!this.gridSchema_existing) return;
const _self = this;
this.gridSchema.cells.forEach(function(el){
_self.grid_cell__add(el);
});
$(this.cell).on("cell:open", function(){
compute_max_height($(this));
})
},
// Init methods
on: function() {
if (!(this.toggleBtn.checked && !this.is_on)) {
return false;
}
this.store = this.cell.querySelector('input[id$="-custom_schema"]');
try {
const storeSchema = JSON.parse(this.store.value);
this.gridSchema_existing = (Object.keys(storeSchema).length) ? storeSchema : undefined;
} catch(e) {
console.error(e);
this.gridSchema_existing = undefined;
}
this.gridSchema = this.gridSchema_existing || this.gridSchema_default;
this.grid__set_layout();
this.grid_cell__init();
this.grid_cell__init_form();
const _self = this;
// Edit grid layout btn click
$(this.edit_grid_btn).on('click', function() {
_self.grid__form_dialog(_self.grid__set_schema.bind(_self));
})
// Add grid cell Btn click
$(this.add_grid_cell_btn).on('click', function() {
_self.grid_cell__form_dialog(gettext('Add'), _self.grid_cell__add_schema.bind(_self));
});
// Grid cells sortable
$(_self.grid_wrapper).sortable({
items: "> .wcs-cards-cell--grid-cell",
placeholder: "wcs-cards-cell--grid-cell-placeholder",
update: function(event, ui) {
ui.item.data('update_index', ui.item.index());
const moved_cell_schema = _self.gridSchema.cells[ui.item.data('start_index')];
_self.gridSchema.cells.splice(ui.item.data('start_index'), 1);
_self.gridSchema.cells.splice(ui.item.data('update_index'), 0, moved_cell_schema);
$(_self.cell).trigger("custom_cell:change");
},
start: function(event, ui) {
ui.item.data("start_index", ui.item.index());
ui.placeholder.addClass(ui.item[0].dataset.cell_size).css('height', ui.item[0].offsetHeight);
}
});
// when schema change
$(this.cell).on("custom_cell:change", function() {
_self.store.value = JSON.stringify(_self.gridSchema);
})
this.is_on = true;
},
init: function() {
const cell = this.cell;
const cardSchema_el = cell.querySelector('[id*="card-schema-eservices"]');
this.cardSchema = cardSchema_el ? JSON.parse(cardSchema_el.innerText) : undefined;
if (!this.cardSchema) {
return;
}
this.is_on = false;
this.grid = cell.querySelector('.wcs-cards-cell--grid');
this.edit_grid_btn = cell.querySelector('.wcs-cards-cell--grid-layout-btn');
this.grid_layout_label = cell.querySelector('.wcs-cards-cell--grid-layout-label');
const grid_form_tpl_string = cell.querySelector('.wcs-cards-cell--grid-form-tpl').innerText;
this.grid_form = this.parse_string_tpl(grid_form_tpl_string);
this.add_grid_cell_btn = cell.querySelector('.wcs-cards-cell--add-grid-cell-btn');
const grid_cell_form_tpl_string = cell.querySelector('.wcs-cards-cell--grid-cell-form-tpl').innerText;
this.grid_cell_form = this.parse_string_tpl(grid_cell_form_tpl_string);
const grid_cell_tpl_string = cell.querySelector('.wcs-cards-cell--grid-cell-tpl').innerText;
this.grid_cell_tpl = this.parse_string_tpl(grid_cell_tpl_string);
this.grid_wrapper = cell.querySelector('.wcs-cards-cell--grid-cells');
this.toggleBtn = this.cell.querySelector('input[id$="-customize_display"]');
const _self = this;
$(this.toggleBtn).on('change', function(e) {
_self.on();
_self.grid_toggle();
}).change();
}
}
// Active custom card UI for each card cell
$(function() {
$('.wcs-card-infos-cell').each(function(i, el) {
const custom_card = new Card_cell_custom(el);
$(el).on('combo:cellform-reloaded', function() {
custom_card.init();
});
})
});

View File

@ -34,4 +34,5 @@
<script src="{% static "js/combo.manager.js" %}"></script>
<script src="{% static "xstatic/leaflet.js" %}"></script>
<script src="{% static "js/combo.map.js" %}"></script>
<script src="{% url "javascript-catalog" %}"></script>
{% endblock %}

View File

@ -15,6 +15,7 @@ from django.test.client import RequestFactory
from django.test.utils import CaptureQueriesContext
from django.urls import reverse
from django.utils.six.moves.urllib import parse as urlparse
from pyquery import PyQuery
from requests.exceptions import ConnectionError
from requests.models import Response
@ -1900,10 +1901,10 @@ def test_card_cell_render(mock_send, context):
mock_send.reset_mock()
result = cell.render(context)
assert '<h2>Card Model 1</h2>' in result
assert '<span class="label">Field A</span>\n \n <span class="value">a</span>' in result
assert '<span class="label">Field B</span>\n \n <span class="value">yes</span>' in result
assert '<span class="label">Field C</span>\n \n <span class="value">Sept. 28, 2020</span>' in result
assert '<span class="label">Related</span>\n \n <span class="value">Foo Bar</span>' in result
assert PyQuery(result).find('span.label:contains("Field A") + span').text() == 'a'
assert PyQuery(result).find('span.label:contains("Field B") + span').text() == 'yes'
assert PyQuery(result).find('span.label:contains("Field C") + span').text() == 'Sept. 28, 2020'
assert PyQuery(result).find('span.label:contains("Related") + span').text() == 'Foo Bar'
assert 'related_raw' not in result
assert 'related_structured' not in result
assert 'Field D' not in result

View File

@ -26,6 +26,7 @@ deps =
pylint
pylint-django
django-webtest<1.9.3
pyquery
psycopg2-binary<2.9
django-mellon>=1.13
vobject