summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-08-14 16:48:10 +0200
committerChristophe Simonis <chs@odoo.com>2019-08-14 16:48:10 +0200
commit140ee6b8f0b0de7ddcc816496269963612c5d0c6 (patch)
tree7bdaa065d215b88fea4fa56adbfa79cdc24deb69 /addons/web/static/src
parent839696d2d7dd574fd8958d17726379926e94d590 (diff)
parent98a55917a602c0c47093b4270ebc4e8b416ecf32 (diff)
[MERGE] forward port branch saas-12.4 up to 98a55917a60
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js2
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js14
-rw-r--r--addons/web/static/src/js/public/public_root_instance.js9
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js89
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js1
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js1
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_controller.js7
-rw-r--r--addons/web/static/src/js/views/control_panel/search/groupby_menu.js5
-rw-r--r--addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js5
-rw-r--r--addons/web/static/src/js/views/graph/graph_controller.js7
-rw-r--r--addons/web/static/src/js/views/graph/graph_renderer.js33
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js38
-rw-r--r--addons/web/static/src/js/widgets/domain_selector.js2
-rw-r--r--addons/web/static/src/js/widgets/model_field_selector.js7
-rw-r--r--addons/web/static/src/scss/list_view.scss3
-rw-r--r--addons/web/static/src/xml/base.xml12
16 files changed, 198 insertions, 37 deletions
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index d226ef655c7..1466402cc98 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1803,8 +1803,8 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
}
this.$('> img').remove();
this.$el.prepend($img);
+
$img.one('error', function () {
- self._clearFile();
$img.attr('src', self.placeholder);
self.do_warn(_t("Image"), _t("Could not display the selected image."));
});
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 58dada8703e..d547ef6849f 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -857,6 +857,8 @@ var FieldX2Many = AbstractField.extend({
toggle_column_order: '_onToggleColumnOrder',
activate_next_widget: '_onActiveNextWidget',
navigation_move: '_onNavigationMove',
+ save_optional_fields: '_onSaveOrLoadOptionalFields',
+ load_optional_fields: '_onSaveOrLoadOptionalFields',
}),
// We need to trigger the reset on every changes to be aware of the parent changes
@@ -1389,6 +1391,18 @@ var FieldX2Many = AbstractField.extend({
});
},
/**
+ * Add necessary key parts for the basic controller to compute the local
+ * storage key. The event will be properly handled by the basic controller.
+ *
+ * @param {OdooEvent} ev
+ * @private
+ */
+ _onSaveOrLoadOptionalFields: function (ev) {
+ ev.data.keyParts.relationalField = this.name;
+ ev.data.keyParts.subViewId = this.view.view_id;
+ ev.data.keyParts.subViewType = this.view.type;
+ },
+ /**
* Forces a resequencing of the records.
*
* @private
diff --git a/addons/web/static/src/js/public/public_root_instance.js b/addons/web/static/src/js/public/public_root_instance.js
index 8202e011cc3..988d939bbe1 100644
--- a/addons/web/static/src/js/public/public_root_instance.js
+++ b/addons/web/static/src/js/public/public_root_instance.js
@@ -1,7 +1,7 @@
odoo.define('root.widget', function (require) {
'use strict';
-require('web.dom_ready');
+var lazyloader = require('web.public.lazyloader');
var rootData = require('web.public.root');
/**
@@ -11,7 +11,10 @@ var rootData = require('web.public.root');
* been consumed.
*/
var publicRoot = new rootData.PublicRoot(null);
-return publicRoot.attachTo(document.body).then(function () {
- return publicRoot;
+return lazyloader.allScriptsLoaded.then(function () {
+ return publicRoot.attachTo(document.body).then(function () {
+ return publicRoot;
+ });
});
+
});
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 2fb6f6400c9..2163f074cb0 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -21,6 +21,8 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
reload: '_onReload',
resequence_records: '_onResequenceRecords',
set_dirty: '_onSetDirty',
+ load_optional_fields: '_onLoadOptionalFields',
+ save_optional_fields: '_onSaveOptionalFields',
sidebar_data_asked: '_onSidebarDataAsked',
translate: '_onTranslate',
}),
@@ -43,6 +45,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
// operations to complete before checking if there are changes to
// discard when discardChanges is called
this.savingDef = Promise.resolve();
+ this.viewId = params.viewId;
},
/**
* @override
@@ -419,6 +422,49 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
});
},
/**
+ * Compute the optional fields local storage key using the given parts.
+ *
+ * @param {Object} keyParts
+ * @param {string} keyParts.viewType view type
+ * @param {string} [keyParts.relationalField] name of the field with subview
+ * @param {integer} [keyParts.subViewId] subview id
+ * @param {string} [keyParts.subViewType] type of the subview
+ * @param {Object} keyParts.fields fields
+ * @param {string} keyParts.fields.name field name
+ * @param {string} keyParts.fields.type field type
+ * @returns {string} local storage key for optional fields in this view
+ * @private
+ */
+ _getOptionalFieldsLocalStorageKey: function (keyParts) {
+ keyParts.model = this.modelName;
+ keyParts.viewType = this.viewType;
+ keyParts.viewId = this.viewId;
+
+ var parts = [
+ 'model',
+ 'viewType',
+ 'viewId',
+ 'relationalField',
+ 'subViewType',
+ 'subViewId',
+ ];
+
+ var viewIdentifier = parts.reduce(function (identifier, partName) {
+ if (partName in keyParts) {
+ return identifier + ',' + keyParts[partName];
+ }
+ return identifier;
+ }, 'optional_fields');
+
+ viewIdentifier =
+ keyParts.fields.sort(this._nameSortComparer)
+ .reduce(function (identifier, field) {
+ return identifier + ',' + field.name;
+ }, viewIdentifier);
+
+ return viewIdentifier;
+ },
+ /**
* Return the params (current_min, limit and size) to pass to the pager,
* according to the current state.
*
@@ -458,6 +504,16 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return true;
},
/**
+ * Sort function used to sort the fields by names, to compute the optional fields keys
+ *
+ * @param {Object} left
+ * @param {Object} right
+ * @private
+ */
+ _nameSortComparer: function(left, right) {
+ return left.name < right.name ? -1 : 1;
+ },
+ /**
* Helper function to display a warning that some fields have an invalid
* value. This is used when a save operation cannot be completed.
*
@@ -676,6 +732,39 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
});
},
/**
+ * Load the optional columns settings in local storage for this view
+ *
+ * @param {OdooEvent} ev
+ * @param {Object} ev.data.keyParts see _getLocalStorageKey
+ * @param {function} ev.data.callback function to call with the result
+ * @private
+ */
+ _onLoadOptionalFields: function (ev) {
+ var res = this.call(
+ 'local_storage',
+ 'getItem',
+ this._getOptionalFieldsLocalStorageKey(ev.data.keyParts)
+ );
+ ev.data.callback(res);
+ },
+ /**
+ * Save the optional columns settings in local storage for this view
+ *
+ * @param {OdooEvent} ev
+ * @param {Object} ev.data.keyParts see _getLocalStorageKey
+ * @param {Array<string>} ev.data.optionalColumnsEnabled list of optional
+ * field names that have been enabled
+ * @private
+ */
+ _onSaveOptionalFields: function (ev) {
+ this.call(
+ 'local_storage',
+ 'setItem',
+ this._getOptionalFieldsLocalStorageKey(ev.data.keyParts),
+ ev.data.optionalColumnsEnabled
+ );
+ },
+ /**
* @private
* @param {OdooEvent} ev
*/
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 00a1be6c228..13ea270870f 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -796,6 +796,7 @@ var BasicModel = AbstractModel.extend({
});
_.each(fields, function (field) {
var dataPoint;
+ record.data[field.name] = null;
if (field.type === 'many2one') {
if (field.value) {
var id = _.isArray(field.value) ? field.value[0] : field.value;
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index 7fd8af532a0..38d2f220c2f 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -48,6 +48,7 @@ var BasicView = AbstractView.extend({
this.controllerParams.archiveEnabled = 'active' in this.fields;
this.controllerParams.hasButtons =
'action_buttons' in params ? params.action_buttons : true;
+ this.controllerParams.viewId = viewInfo.view_id;
this.loadParams.fieldsInfo = this.fieldsInfo;
this.loadParams.fields = this.fields;
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index a0de1c665f5..edb8e54e0b7 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -55,6 +55,7 @@ var CalendarController = AbstractController.extend({
this.readonlyFormViewId = params.readonlyFormViewId;
this.mapping = params.mapping;
this.context = params.context;
+ this.previousOpen = null;
// The quickCreating attribute ensures that we don't do several create
this.quickCreating = false;
},
@@ -285,7 +286,8 @@ var CalendarController = AbstractController.extend({
title += ': ' + this.renderer.arch.attrs.string;
}
if (this.eventOpenPopup) {
- new dialogs.FormViewDialog(self, {
+ if (this.previousOpen) { this.previousOpen.close(); }
+ this.previousOpen = new dialogs.FormViewDialog(self, {
res_model: this.modelName,
context: context,
title: title,
@@ -297,7 +299,8 @@ var CalendarController = AbstractController.extend({
}
self.reload();
},
- }).open();
+ });
+ this.previousOpen.open();
} else {
this.do_action({
type: 'ir.actions.act_window',
diff --git a/addons/web/static/src/js/views/control_panel/search/groupby_menu.js b/addons/web/static/src/js/views/control_panel/search/groupby_menu.js
index 982ac097b5c..a41bd1c23b5 100644
--- a/addons/web/static/src/js/views/control_panel/search/groupby_menu.js
+++ b/addons/web/static/src/js/views/control_panel/search/groupby_menu.js
@@ -31,6 +31,7 @@ var GroupByMenu = DropdownMenu.extend({
*/
init: function (parent, groupBys, fields, options) {
var self = this;
+ options = options || {};
this._super(parent, groupBys);
this.fields = fields;
// determines when the 'Add custom groupby' submenu is open
@@ -55,9 +56,9 @@ var GroupByMenu = DropdownMenu.extend({
this.dropdownCategory = 'groupby';
this.dropdownTitle = _t('Group By');
this.dropdownIcon = 'fa fa-bars';
- this.dropdownSymbol = this.isMobile ? 'fa fa-chevron-right float-right mt4' : false;
+ this.dropdownSymbol = this.isMobile && !options.noSymbol ? 'fa fa-chevron-right float-right mt4' : false;
// the default style of the groupby menu can be changed here using the options key 'headerStyle'
- if (options && options.headerStyle === 'primary') {
+ if (options.headerStyle === 'primary') {
this.dropdownStyle = {
el: {class: 'btn-group o_group_by_menu o_dropdown', attrs: {'role': 'group'}},
mainButton: {class: 'btn btn-primary dropdown-toggle'},
diff --git a/addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js b/addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js
index 5c421a6e7ee..a51386d20aa 100644
--- a/addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js
+++ b/addons/web/static/src/js/views/control_panel/search/groupby_menu_mixin.js
@@ -36,13 +36,14 @@ var GroupByMenuMixin = {
* private
* @param {jQuery} $node
* @param {Object} groupableFields
+ * @param {Promise}
*/
_addGroupByMenu: function ($node, groupableFields) {
this.sortedFieldNames = Object.keys(groupableFields).sort();
this.groupableFields = groupableFields;
const groupBys = this._getGroupBys(this.model.get().groupBy);
- this.groupByMenu = new GroupByMenu(this, groupBys, this.groupableFields);
- this.groupByMenu.insertAfter($node.find('div:first'));
+ this.groupByMenu = new GroupByMenu(this, groupBys, this.groupableFields, {noSymbol: true});
+ return this.groupByMenu.insertAfter($node.find('div:first'));
},
/**
* This method puts the active groupBys in a convenient form.
diff --git a/addons/web/static/src/js/views/graph/graph_controller.js b/addons/web/static/src/js/views/graph/graph_controller.js
index 8cfed52a050..2953b99f731 100644
--- a/addons/web/static/src/js/views/graph/graph_controller.js
+++ b/addons/web/static/src/js/views/graph/graph_controller.js
@@ -96,8 +96,13 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{
this._updateButtons();
this.$buttons.appendTo($node);
if (this.isEmbedded) {
- this._addGroupByMenu($node, this.groupableFields);
+ this._addGroupByMenu($node, this.groupableFields).then(function(){
+ var groupByButton = $node.find('.o_dropdown_toggler_btn');
+ groupByButton.removeClass("o_dropdown_toggler_btn btn btn-secondary dropdown-toggle");
+ groupByButton.addClass("btn dropdown-toggle btn-outline-secondary");
+ });
}
+
}
},
diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js
index ea94020388f..e24de02879c 100644
--- a/addons/web/static/src/js/views/graph/graph_renderer.js
+++ b/addons/web/static/src/js/views/graph/graph_renderer.js
@@ -314,6 +314,24 @@ return AbstractRenderer.extend({
return this.state.origins[dataPt.originIndex];
},
/**
+ * Returns an object used to style chart elements independently from the datasets.
+ *
+ * @private
+ * @returns {Object}
+ */
+ _getElementOptions: function () {
+ var elementOptions = {};
+ if (this.state.mode === 'bar') {
+ elementOptions.rectangle = {borderWidth: 1};
+ } else if (this.state.mode === 'line') {
+ elementOptions.line = {
+ tension: 0,
+ fill: false,
+ };
+ }
+ return elementOptions;
+ },
+ /**
* Returns a DateClasses instance used to manage equivalence of dates.
*
* @private
@@ -348,9 +366,8 @@ return AbstractRenderer.extend({
} else {
return dataPt.labels.slice(0, 1);
}
- } else if (i >= 0) {
+ } else if (i === 0) {
return Array.prototype.concat.apply([], [
- dataPt.labels.slice(0, i),
this.dateClasses.dateClass(dataPt.originIndex, dataPt.labels[i]),
dataPt.labels.slice(i+1)
]);
@@ -667,6 +684,7 @@ return AbstractRenderer.extend({
scales: this._getScaleOptions(),
legend: this._getLegendOptions(datasetsCount),
tooltips: this._getTooltipOptions(),
+ elements: this._getElementOptions(),
};
},
/**
@@ -687,7 +705,7 @@ return AbstractRenderer.extend({
if (_.contains(['bar', 'line'], this.state.mode) && i === 0) {
// here label is an array of length 1 and contains a number
return this.dateClasses.representative(label, originIndex) || '';
- } else if (this.state.mode === 'pie' && i >= 0) {
+ } else if (this.state.mode === 'pie' && i === 0) {
// here label is an array of length at least one containing string or numbers
var labelCopy = label.slice(0);
if (originIndex !== undefined) {
@@ -737,7 +755,7 @@ return AbstractRenderer.extend({
this.$el.append($canvasContainer);
var i = this.state.comparisonFieldIndex;
- if (i === 0 || (i > 0 && this.state.mode === 'pie')) {
+ if (i === 0) {
this.dateClasses = this._getDateClasses(dataPoints);
}
if (this.state.mode === 'bar') {
@@ -761,9 +779,6 @@ return AbstractRenderer.extend({
_renderBarChart: function (dataPoints) {
var self = this;
- // style rectangles
- Chart.defaults.global.elements.rectangle.borderWidth = 1;
-
// prepare data
var data = this._prepareData(dataPoints);
@@ -820,10 +835,6 @@ return AbstractRenderer.extend({
_renderLineChart: function (dataPoints) {
var self = this;
- // style lines
- Chart.defaults.global.elements.line.tension = 0;
- Chart.defaults.global.elements.line.fill = false;
-
// prepare data
var data = this._prepareData(dataPoints);
data.datasets.forEach(function (dataset, index) {
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 845014926b3..2ddf21b1ef8 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -70,7 +70,16 @@ var ListRenderer = BasicRenderer.extend({
this.editable = params.editable;
this.isGrouped = this.state.groupedBy.length > 0;
this.groupbys = params.groupbys;
+ },
+ /**
+ * Compute columns visilibity. This can't be done earlier as we need the
+ * controller to respond to the load_optional_fields call of processColumns.
+ *
+ * @override
+ */
+ willStart: function() {
this._processColumns(this.columnInvisibleFields || {});
+ return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
@@ -241,14 +250,13 @@ var ListRenderer = BasicRenderer.extend({
* @private
* @returns {string}
*/
- _getOptionalColumnsStorageKey: function () {
+ _getOptionalColumnsStorageKeyParts: function () {
var self = this;
- var fields = [];
- _.each(this.state.fieldsInfo[this.viewType], function (field, name) {
- fields.push(name + ":" + self.state.fields[name].type);
- });
- fields.sort();
- return "list_optional_fields," + this.state.model + "," + this.viewType + "," + fields.join(',');
+ return {
+ fields: _.map(this.state.fieldsInfo[this.viewType], function (_, fieldName) {
+ return {name: fieldName, type: self.state.fields[fieldName].type};
+ }),
+ }
},
/**
* Removes the columns which should be invisible.
@@ -261,8 +269,13 @@ var ListRenderer = BasicRenderer.extend({
this.columns = [];
this.optionalColumns = [];
this.optionalColumnsEnabled = [];
- var localStorageKey = this._getOptionalColumnsStorageKey();
- var storedOptionalColumns = this.call('local_storage', 'getItem', localStorageKey);
+ var storedOptionalColumns;
+ this.trigger_up('load_optional_fields', {
+ keyParts: this._getOptionalColumnsStorageKeyParts(),
+ callback: function(res) {
+ storedOptionalColumns = res;
+ },
+ });
_.each(this.arch.children, function (c) {
if (c.tag !== 'control' && c.tag !== 'groupby') {
var reject = c.attrs.modifiers.column_invisible;
@@ -750,7 +763,7 @@ var ListRenderer = BasicRenderer.extend({
}
if (field.type === 'float' || field.type === 'integer' || field.type === 'monetary') {
- $th.css({ textAlign: 'right' });
+ $th.addClass('o_list_number_th');
}
if (config.isDebug()) {
@@ -1032,7 +1045,10 @@ var ListRenderer = BasicRenderer.extend({
} else {
this.optionalColumnsEnabled.push(input.name);
}
- this.call('local_storage', 'setItem', this._getOptionalColumnsStorageKey(), this.optionalColumnsEnabled);
+ this.trigger_up('save_optional_fields', {
+ keyParts: this._getOptionalColumnsStorageKeyParts(),
+ optionalColumnsEnabled: this.optionalColumnsEnabled,
+ });
this._processColumns(this.columnInvisibleFields || {});
this._renderView().then(function() {
self._onToggleOptionalColumnDropdown(ev);
diff --git a/addons/web/static/src/js/widgets/domain_selector.js b/addons/web/static/src/js/widgets/domain_selector.js
index ed71679699c..755ee32979e 100644
--- a/addons/web/static/src/js/widgets/domain_selector.js
+++ b/addons/web/static/src/js/widgets/domain_selector.js
@@ -652,7 +652,7 @@ var DomainLeaf = DomainNode.extend({
this.fieldSelector = new ModelFieldSelector(
this,
this.model,
- this.chain ? this.chain.split(".") : [],
+ this.chain !== undefined ? this.chain.toString().split(".") : [],
this.options
);
defs.push(this.fieldSelector.appendTo($("<div/>")).then((function () {
diff --git a/addons/web/static/src/js/widgets/model_field_selector.js b/addons/web/static/src/js/widgets/model_field_selector.js
index 08a7ae47c81..ed74b2da44a 100644
--- a/addons/web/static/src/js/widgets/model_field_selector.js
+++ b/addons/web/static/src/js/widgets/model_field_selector.js
@@ -301,11 +301,16 @@ var ModelFieldSelector = Widget.extend({
}).bind(this));
function processChain(chain) {
- var field = this._getLastPageField(chain.pop());
+ var fieldName = chain.pop();
+ var field = this._getLastPageField(fieldName);
if (field && field.relation && chain.length > 0) { // Fetch next chain node if any and possible
return this._pushPageData(field.relation).then(processChain.bind(this, chain));
} else if (field && chain.length === 0) { // Last node fetched
return Promise.resolve();
+ } else if (!field && fieldName === "1") { // TRUE_LEAF
+ this._validate(true);
+ } else if (!field && fieldName === "0") { // FALSE_LEAF
+ this._validate(true);
} else { // Wrong node chain
this._validate(false);
}
diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss
index 0311747ebe8..a2ab20f309f 100644
--- a/addons/web/static/src/scss/list_view.scss
+++ b/addons/web/static/src/scss/list_view.scss
@@ -22,6 +22,9 @@
&:not(.o_column_sortable):hover {
cursor: default;
}
+ &.o_list_number_th {
+ text-align: right;
+ }
}
}
tbody > tr > td:not(.o_list_record_selector) {
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 05553614b7c..300aff10c00 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -643,8 +643,16 @@
<i class="fa fa-chevron-right" role="img" aria-label="Followed by" title="Followed by"/>
</t>
<span class="o_field_selector_chain_part">
- <t t-set="fieldInfo" t-value="_.findWhere(pages[fieldName_index], {'name': fieldName})"/>
- <t t-esc="fieldInfo &amp;&amp; fieldInfo.string || '?'"/>
+ <t t-if="fieldName === '1'">
+ <t t-esc="fieldName"/>
+ </t>
+ <t t-elif="fieldName === '0'">
+ <t t-esc="fieldName"/>
+ </t>
+ <t t-else="">
+ <t t-set="fieldInfo" t-value="_.findWhere(pages[fieldName_index], {'name': fieldName})"/>
+ <t t-esc="fieldInfo &amp;&amp; fieldInfo.string || '?'"/>
+ </t>
</span>
</t>
</t>