summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/basic
diff options
context:
space:
mode:
authorJérémy Hennecart <jeh@odoo.com>2020-06-19 07:06:31 +0000
committerAaron Bohy <aab@odoo.com>2020-06-22 13:04:01 +0000
commitc7964657ecbbad278cec73cf231d7eef55b44347 (patch)
treebe77532a80cbed1283c2062cb2dc1159aa0fbdd4 /addons/web/static/src/js/views/basic
parent96fcefb6f1fe3fb5a5f197e2635e6d71d9a1d9dd (diff)
[IMP] web: add support of "sample" attribute in views
With this commit, it is now possible to display sample (fake) data in empty views, in the hope of easing user onboarding. This can be enabled (in list and kanban views) with attribute sample="1" on the arch root element. In this case, if there is no data to display, sample data will be generated based on heuristics (depending on field types and names). This can be used in addition to the no content helper. Task 2232801 X-original-commit: 7c8e627ff5029cb539d702705e5ce53d658c76ed Co-authored-by: Aaron Bohy <aab@odoo.com> Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com> Co-authored-by: Jérémy Hennecart <jeh@odoo.com> Co-authored-by: Julien Mougenot <jum@odoo.com>
Diffstat (limited to 'addons/web/static/src/js/views/basic')
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js1
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js82
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js2
-rw-r--r--addons/web/static/src/js/views/basic/basic_view.js3
4 files changed, 76 insertions, 12 deletions
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 99da98f4301..0849cf5508d 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -43,7 +43,6 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
this.hasButtons = params.hasButtons;
FieldManagerMixin.init.call(this, this.model);
this.mode = params.mode || 'readonly';
- this.handle = this.initialState.id;
// savingDef is used to ensure that we always wait for pending save
// operations to complete before checking if there are changes to
// discard when discardChanges is called
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 9771fcc63e9..5b58dc4d7e7 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -164,6 +164,13 @@ var BasicModel = AbstractModel.extend({
this.batchedRPCsRequests = [];
this.localData = Object.create(null);
+ // used to generate dataPoint ids. Note that the counter is set to 0 for
+ // each instance, and this is mandatory for the sample data feature to
+ // work: we need both the main model and the sample model to generate the
+ // same datapoint ids for their common data (groups, when there are real
+ // groups in database), so that we can easily do the mapping between
+ // real and sample data.
+ this.__id = 0;
this._super.apply(this, arguments);
},
@@ -549,7 +556,7 @@ var BasicModel = AbstractModel.extend({
this.localData[listID].orderedResIDs = list.res_ids;
},
/**
- * The get method first argument is the handle returned by the load method.
+ * The __get method first argument is the handle returned by the load method.
* It is optional (the handle can be undefined). In some case, it makes
* sense to use the handle as a key, for example the BasicModel holds the
* data for various records, each with its local ID.
@@ -563,7 +570,7 @@ var BasicModel = AbstractModel.extend({
* @param {boolean} [options.raw=false] if true, will not follow relations
* @returns {Object}
*/
- get: function (id, options) {
+ __get: function (id, options) {
var self = this;
options = options || {};
@@ -601,7 +608,7 @@ var BasicModel = AbstractModel.extend({
relDataPoint = this.localData[data[fieldName]];
data[fieldName] = relDataPoint ? relDataPoint.res_id : false;
} else {
- data[fieldName] = this.get(data[fieldName]) || false;
+ data[fieldName] = this.__get(data[fieldName]) || false;
}
} else if (field.type === 'reference') {
if (options.raw) {
@@ -610,7 +617,7 @@ var BasicModel = AbstractModel.extend({
relDataPoint.model + ',' + relDataPoint.res_id :
false;
} else {
- data[fieldName] = this.get(data[fieldName]) || false;
+ data[fieldName] = this.__get(data[fieldName]) || false;
}
} else if (field.type === 'one2many' || field.type === 'many2many') {
if (options.raw) {
@@ -625,7 +632,7 @@ var BasicModel = AbstractModel.extend({
data[fieldName] = data[fieldName] || [];
}
} else {
- data[fieldName] = this.get(data[fieldName]) || [];
+ data[fieldName] = this.__get(data[fieldName]) || [];
}
}
}
@@ -687,7 +694,7 @@ var BasicModel = AbstractModel.extend({
context: _.extend({}, element.context),
count: element.count,
data: _.map(element.data, function (elemID) {
- return self.get(elemID, options);
+ return self.__get(elemID, options);
}),
domain: element.domain.slice(0),
fields: element.fields,
@@ -701,6 +708,7 @@ var BasicModel = AbstractModel.extend({
id: element.id,
isDirty: element.isDirty,
isOpen: element.isOpen,
+ isSample: this.isSampleModel,
limit: element.limit,
model: element.model,
offset: element.offset,
@@ -751,6 +759,27 @@ var BasicModel = AbstractModel.extend({
return isDirty;
},
/**
+ * Returns true iff the datapoint is of type list and either:
+ * - is not grouped, and contains no records
+ * - is grouped, and contains columns, but all columns are empty
+ * In these cases, we will generate sample data to display, instead of an
+ * empty state.
+ *
+ * @override
+ */
+ _isEmpty(dataPointID) {
+ const dataPoint = this.localData[dataPointID];
+ if (dataPoint.type === 'list') {
+ const hasRecords = dataPoint.count === 0;
+ if (dataPoint.groupedBy.length) {
+ return dataPoint.data.length > 0 && hasRecords;
+ } else {
+ return hasRecords;
+ }
+ }
+ return false;
+ },
+ /**
* Check if a localData is new, meaning if it is in the process of being
* created and no actual record exists in db. Note: if the localData is not
* of the "record" type, then it is always considered as not new.
@@ -782,6 +811,7 @@ var BasicModel = AbstractModel.extend({
*
* @todo document all params
*
+ * @private
* @param {any} params
* @param {Object} [params.fieldsInfo={}] contains the fieldInfo of each field
* @param {Object} params.fields contains the description of each field
@@ -789,7 +819,8 @@ var BasicModel = AbstractModel.extend({
* @param {string} [params.recordID] an ID for an existing resource.
* @returns {Promise<string>} resolves to a local id, or handle
*/
- load: function (params) {
+ __load: async function (params) {
+ await this._super(...arguments);
params.type = params.type || (params.res_id !== undefined ? 'record' : 'list');
// FIXME: the following seems only to be used by the basic_model_tests
// so it should probably be removed and the tests should be adapted
@@ -954,13 +985,15 @@ var BasicModel = AbstractModel.extend({
* Reload all data for a given resource. At any time there is at most one
* reload operation active.
*
+ * @private
* @param {string} id local id for a resource
* @param {Object} [options]
* @param {boolean} [options.keepChanges=false] if true, doesn't discard the
* changes on the record before reloading it
* @returns {Promise<string>} resolves to the id of the resource
*/
- reload: function (id, options) {
+ __reload: async function (id, options) {
+ await this._super(...arguments);
return this.mutex.exec(this._reload.bind(this, id, options));
},
/**
@@ -2196,6 +2229,20 @@ var BasicModel = AbstractModel.extend({
return hasOnchange ? specs : false;
},
/**
+ * Ensures that dataPoint ids are always synchronized between the main and
+ * sample models when being in sample mode. Here, we now that __id in the
+ * sample model is always greater than __id in the main model (as it
+ * contains strictly more datapoints).
+ *
+ * @override
+ */
+ async _callSampleModel() {
+ await this._super(...arguments);
+ if (this._isInSampleMode) {
+ this.__id = this.sampleModel.__id;
+ }
+ },
+ /**
* Compute the default value that the handle field should take.
* We need to compute this in order for new lines to be added at the correct position.
*
@@ -3968,7 +4015,7 @@ var BasicModel = AbstractModel.extend({
groupsCount: 0,
groupsLimit: type === 'list' && params.groupsLimit || null,
groupsOffset: 0,
- id: _.uniqueId(params.modelName + '_'),
+ id: `${params.modelName}_${++this.__id}`,
isOpen: params.isOpen,
limit: type === 'record' ? 1 : (params.limit || Number.MAX_SAFE_INTEGER),
loadMoreOffset: 0,
@@ -4848,6 +4895,23 @@ var BasicModel = AbstractModel.extend({
});
},
/**
+ * Override to handle the case where we want sample data, and we are in a
+ * grouped kanban or list view with real groups, but all groups are empty.
+ * In this case, we use the result of the web_read_group rpc to tweak the
+ * data in the SampleServer instance of the sampleModel (so that calls to
+ * that server will return the same groups).
+ *
+ * @override
+ */
+ async _rpc(params) {
+ const result = await this._super(...arguments);
+ if (this.sampleModel && params.method === 'web_read_group' && result.length) {
+ const sampleServer = this.sampleModel.sampleServer;
+ sampleServer.setExistingGroups(result.groups);
+ }
+ return result;
+ },
+ /**
* Allows to save a value in the specialData cache associated to a given
* record and fieldName. If the value in the cache was already the given
* one, nothing is done and the method indicates it by returning false
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index fd285010054..8db6058f242 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -455,7 +455,7 @@ var BasicRenderer = AbstractRenderer.extend(WidgetAdapterMixin, {
* @returns {boolean}
*/
_hasContent: function () {
- return this.state.count !== 0;
+ return this.state.count !== 0 && (!('isSample' in this.state) || !this.state.isSample);
},
/**
* Force the resequencing of the records after moving one of them to a given
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 f1e093e0479..eb6ef5a2549 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -197,7 +197,8 @@ var BasicView = AbstractView.extend({
}
}
return Promise.resolve(def).then(function () {
- return model.get(record.id);
+ const handle = record.id;
+ return { state: model.get(handle), handle };
});
}
return this._super.apply(this, arguments);