      1 /*
      2  *  Copyright (C) 2009 WaveMaker Software, Inc.
      3  *
      4  *  This file is part of the WaveMaker Client Runtime.
      5  *
      6  *  Licensed under the Apache License, Version 2.0 (the "License");
      7  *  you may not use this file except in compliance with the License.
      8  *  You may obtain a copy of the License at
      9  *
     10  *      http://www.apache.org/licenses/LICENSE-2.0
     11  *
     12  *  Unless required by applicable law or agreed to in writing, software
     13  *  distributed under the License is distributed on an "AS IS" BASIS,
     14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     15  *  See the License for the specific language governing permissions and
     16  *  limitations under the License.
     17  */
     18 dojo.provide("wm.base.components.LiveVariable");
     19 dojo.require("wm.base.components.ServiceVariable");
     20 dojo.require("wm.base.components.LiveView");
     21
     22 /**
     23 	Component that marshalls a LiveView and can perform all data operations: read, insert, update, delete.
     24 	@name wm.LiveVariable
     25 	@class
     26 	@extends wm.ServiceVariable
     27 */
     28 dojo.declare("wm.LiveVariable", wm.ServiceVariable, {
     29 	/**
     30 		@lends wm.LiveVariable.prototype
     31 	*/
     32 	autoUpdate: true,
     33 	startUpdate: true,
     34 	operation: "read",
     35 	/** First row of results */
     36 	firstRow: 0,
     37 	/** Optional starting source data */
     38 	sourceData: null,
     39 	/** Method by which data is filtered */
     40 	matchMode: "start",
     41 	/** Toggle for data sorting to ignore alphabetical case or not. */
     42 	ignoreCase: true,
     43 	/** Optional order by clause, example "asc: cityId, desc: city" */
     44 	orderBy: "",
     45 	/** LiveView or LiveTable from which this LiveVariable gets its field information; can use a liveView or liveTable */
     46 	liveSource: null,
     47 	/** our liveView **/
     48 	liveView: null,
     49 	/** Maximum number of results to return */
     50 	maxResults: 0,
     51 	designMaxResults: 500,
     52 	/** Field in view to use as our root object / type */
     53 	_rootField: "",
     54 	destroy: function() {
     55 		this._unsubscribeLiveView();
     56 		this.inherited(arguments);
     57 	},
     58 	init: function() {
     59 		this.inherited(arguments);
     60 		this.filter = new wm.Variable({name: "filter", owner: this, type: this.type || "any" });
     61 		this.sourceData = new wm.Variable({name: "sourceData", owner: this, type: this.type || "any" });
     62 		this.subscribe(this.filter.getRuntimeId() + "-changed", this, "filterChanged");
     63 		this.subscribe(this.sourceData.getRuntimeId() + "-changed", this, "sourceDataChanged");
     64 	},
     65 	postInit: function() {
     66 		this.inherited(arguments);
     67 		// initialize via liveSource or optionally directly with a liveView)
     68 		if (this.liveSource)
     69 			this.setLiveSource(this.liveSource);
     70 		else
     71 			this.setLiveView(this.liveView || this.createLiveView(this.type));
     72 		this.doAutoUpdate();
     73 	},
     74 	_subscribeLiveView: function() {
     75 		this._unsubscribeLiveView();
     76 		if (this.liveView)
     77 			this._liveViewSubscription = dojo.subscribe(this.liveView.getRuntimeId() + "-viewChanged", dojo.hitch(this, "_liveViewChanged"));
     78 	},
     79 	_unsubscribeLiveView: function() {
     80 		dojo.unsubscribe(this._liveViewSubscription);
     81 		this._liveViewSubscription = null;
     82 	},
     83 	isLiveType: function() {
     84 		return wm.typeManager.getLiveService(this.type);
     85 	},
     86 	doAutoUpdate: function() {
     87 		if (this.isLiveType())
     88 			this.inherited(arguments);
     89 	},
     90 	filterChanged: function() {
     91 		this.doAutoUpdate();
     92 	},
     93 	sourceDataChanged: function() {
     94 		this.doAutoUpdate();
     95 	},
     96 	/** Set the filter used for read operations */
     97 	setFilter: function(inFilter) {
     98 		if ((inFilter || 0).type == this.type) {
     99 			this.filter.setDataSet(inFilter);
    100 		}
    101 	},
    102 	/** Set the orderBy property used for read operations */
    103 	setOrderBy: function(inOrderBy) {
    104 		this.orderBy = inOrderBy;
    105 		this.doAutoUpdate();
    106 	},
    107 	/** Set the source data which is used for operations. */
    108 	setSourceData: function(inSourceData) {
    109 		var liveType = this.isLiveType();
    110 		if (!liveType || (inSourceData || 0).type == this.type) {
    111 			this.sourceData.setDataSet(inSourceData);
    112 			if (!liveType) {
    113 				this._updating++;
    114 				this.setLiveSource(this.sourceData.type);
    115 				this._updating--;
    116 			}
    117 		}
    118 	},
    119 	// ==========================================================
    120 	// LiveView integration
    121 	// ==========================================================
    122 	/** Set the LiveView or LiveTable from which we will get data information */
    123 	/* valid input: LiveView full id or LiveTable full name */
    124 	setLiveSource: function(inLiveSource) {
    125 		var
    126 			s =this.liveSource = inLiveSource,
    127 			v = this.getRoot().app.getValueById(s) || this.createLiveView(s);
    128 		if (v)
    129 			this.setLiveView(v);
    130 		this.doAutoUpdate();
    131 	},
    132 	setLiveView: function(inLiveView) {
    133 		this.clearData();
    134 		this.liveView = inLiveView;
    135 		this._subscribeLiveView();
    136 		this.setType(this.getViewType());
    137 	},
    138 	createLiveView: function(inType) {
    139 		return new wm.LiveView({
    140 			name: "liveView",
    141 			owner: this,
    142 			dataType: inType,
    143 			_defaultView: true
    144 		});
    145 	},
    146 	setType: function(inType) {
    147 		this.inherited(arguments);
    148 		this.filter.setType(this.type);
    149 		this.sourceData.setType(this.type);
    150 		if (!this._updating && this.$.binding)
    151 			this.$.binding.refresh();
    152 	},
    153 	_liveViewChanged: function() {
    154 		this.setType(this.liveView.dataType);
    155 		if (this.isDesignLoaded())
    156 			this.doAutoUpdate();
    157 	},
    158 	// ==========================================================
    159 	// Server I/O
    160 	// ==========================================================
    161 	_getCanUpdate: function() {
    162 		return this.inherited(arguments) &&
    163 			!(this.operation == "read" && this._isSourceDataBound() && wm.isEmpty(this.sourceData.getData()) );
    164 	},
    165 	// FIXME: need to zot this
    166 	operationChanged: function() {
    167 	},
    168 	_update: function() {
    169 		// note: runtime service only available when application is deployed
    170 		// so must wait until here to set it.
    171 		this._service = wm.getRuntimeService(this);
    172 		//console.log(this.name, "update");
    173 		return this.inherited(arguments);
    174 	},
    175 	getArgs: function() {
    176 		var
    177 			d = this.sourceData.getData(),
    178 			t = this.sourceData.type || this.type,
    179 			s = wm.typeManager.getService(this.type),
    180 			args = [s, t, wm.isEmpty(d) ? null : d];
    181 		if (this.operation == "read") {
    182 			args = args.concat(this._getReadArguments());
    183 		}
    184 		return args;
    185 	},
    186 	_getReadArguments: function() {
    187 		var
    188 			props = {properties: this._getEagerProps(this), filters: this._getFilters(), matchMode: this.matchMode, ignoreCase: this.ignoreCase},
    189 			paging = this.orderBy ? {orderBy: (this.orderBy || "").split(",")} : {},
    190 			max = this.isDesignLoaded() ? this.designMaxResults : this.maxResults,
    191 			results = max ? { maxResults: max, firstResult: this.firstRow } : {};
    192 		dojo.mixin(paging, results);
    193 		return [props, paging];
    194 	},
    195 	_getFilters: function() {
    196 		return this._getFilterValues(this.filter.getData());
    197 	},
    198 	_getFilterValues: function(inData, inPrefix) {
    199 		var f = [], d, p;
    200 		for (var i in inData) {
    201 			d = inData[i];
    202 			p = (inPrefix ? (inPrefix ||"") + "." : "") + i;
    203 			if (dojo.isObject(d) && d !== null)
    204 				f = f.concat(this._getFilterValues(d, p));
    205 			else if (p !== undefined && d !== undefined)
    206 				f.push(p + "=" + d);
    207 		}
    208 		return f;
    209 	},
    210 	_isSourceDataBound: function() {
    211 		var wires = this.$.binding.wires, w;
    212 		for (var i in wires) {
    213 			w = wires[i];
    214 			if ((w.targetProperty || "").indexOf("sourceData") == 0)
    215 				return true;
    216 		}
    217 	},
    218 	processResult: function(inResult) {
    219 		this.dataSetCount = this._service.fullResult.dataSetSize;
    220 		this.inherited(arguments);
    221 	},
    222 	//===========================================================================
    223 	// Paging
    224 	//===========================================================================
    225 	/** Return the current data page; only relevant when maxResults is set. */
    226 	getPage: function() {
    227 		return Math.floor(this.firstRow / (this.maxResults || 1));
    228 	},
    229 	/** Return the total number of data pages. */
    230 	getTotalPages: function() {
    231 		return Math.ceil((this.dataSetCount || 1) / (this.maxResults || 1));
    232 	},
    233 	/** Set and retrieve the current data page.
    234 		@param {Number} inPageIndex the page number to set
    235 	 */
    236 	setPage: function(inPageIndex) {
    237 		inPageIndex = Math.max(0, Math.min(this.getTotalPages()-1, inPageIndex));
    238 		this.firstRow = inPageIndex * (this.maxResults || 0);
    239 		this.update();
    240 	},
    241 	/** Set and retrieve the next page of data. */
    242 	setNextPage: function() {
    243 		this.setPage(this.getPage()+1);
    244 	},
    245 	/** Set and retrieve the previous page of data. */
    246 	setPreviousPage: function() {
    247 		this.setPage(this.getPage()-1);
    248 	},
    249 	/** Set and retrieve the first page of data. */
    250 	setFirstPage: function() {
    251 		this.setPage(0);
    252 	},
    253 	/** Set and retrieve the last page of data. */
    254 	setLastPage: function() {
    255 		this.setPage(this.getTotalPages()-1);
    256 	}
    257 });
    258
    259 //===========================================================================
    260 // Design Only
    261 //===========================================================================
    262
    263 wm.Object.extendSchema(wm.LiveVariable, {
    264 	update: {ignore: 1, publicEvent: 1},
    265 	related: { ignore: 1},
    266 	view: { ignore: 1},
    267 	service: { ignore: 1},
    268 	dataType: { ignore: 1},
    269 	operation: { group: "data", order: 0},
    270 	input: {ignore: 1},
    271 	liveSource: { group: "data", order: 1},
    272 	liveView: { ignore: 1},
    273 	sourceData: {ignore: 1, group: "data", order: 3, bindTarget: 1, categoryParent: "Properties", categoryProps: {component: "sourceData", inspector: "Data"}},
    274 	filter: { ignore: 1, group: "data", order: 5, bindTarget: 1, categoryParent: "Properties", categoryProps: {component: "filter", inspector: "Data"}},
    275 	matchMode: {group: "data", order: 10},
    276 	firstRow: {group: "data", order: 15},
    277 	maxResults: {group: "data", order: 17},
    278 	designMaxResults: {group: "data", order: 18},
    279 	orderBy: {group: "data", order: 19},
    280 	ignoreCase:  {group: "data", order: 20},
    281 	configure: { ignore: 1 },
    282 	dataSetCount: { ignore: 1 }
    283 });
    284
    285 wm.LiveVariable.extend({
    286 	_operations: ["read", "insert", "update", "delete"],
    287 	_matchModes: ["start", "end", "anywhere", "exact"],
    288 	listProperties: function() {
    289 		var
    290 			p = this.inherited(arguments),
    291 			r = (this.operation == "read");
    292 		p.matchMode.ignore = !r;
    293 		p.firstRow.ignore = !r;
    294 		p.maxResults.ignore = !r;
    295 		p.designMaxResults.ignore = !r;
    296 		p.orderBy.ignore = !r;
    297 		p.ignoreCase.ignore = !r;
    298 		p.filter.bindTarget = r;
    299 		p.filter.categoryParent = r ? "Properties" : "";
    300 		return p;
    301 	},
    302 	isListBindable: function() {
    303 		return this.operation == "read" ? !(this.sourceData && !wm.isEmpty(this.sourceData.getData())) : false;
    304 	},
    305 	designCreate: function() {
    306 		this.inherited(arguments);
    307 		this.subscribe("wmwidget-idchanged", this, "componentNameChanged");
    308
    309 	},
    310 	componentNameChanged: function(inOldId, inNewId, inOldRtId, inNewRtId) {
    311 		if (inOldId == this.liveSource)
    312 			this.setLiveSource(inNewId);
    313 	},
    314 	set_operation: function(inOperation) {
    315 		this.operation = inOperation;
    316 		// just a good idea for safety
    317 		if (this.isDesignLoaded()) {
    318 			// automatically set autoUpdate to true if we're reading,
    319 			// since this is the default anyway, otherwise set to false.
    320 			this.setAutoUpdate(inOperation == "read");
    321 			if (studio.selected == this)
    322 				studio.inspector.inspect(this);
    323 		}
    324 	},
    325 	set_liveSource: function(inLiveSource) {
    326 		this.setLiveSource(inLiveSource);
    327 		if (this.isDesignLoaded() && studio.selected == this)
    328 			studio.inspector.inspect(this);
    329 	},
    330 	set_sourceData: function(inSourceData) {
    331 		this.setSourceData(inSourceData);
    332 		if (this.isDesignLoaded() && studio.selected == this)
    333 			studio.inspector.inspect(this);
    334 	},
    335 	set_filter: function(inFilter) {
    336 		this.setFilter(inFilter);
    337 		if (this.isDesignLoaded() && studio.selected == this)
    338 			studio.inspector.inspect(this);
    339 	},
    340 	checkOrderBy: function(inOrderBy) {
    341 		var
    342 			orderParts = (inOrderBy || "").split(','),
    343 			re = new RegExp("^(?:asc|desc)\:", "i");
    344 		for (var i=0, o; (o = orderParts[i]); i++)
    345 			if (!dojo.trim(o).match(re)) {
    346 				alert("Each property used in the orderBy clause must be of the form asc|desc: <propertyPath>. \"" + o + "\" does not match this format." +
    347 					" The current orderBy clause will generate an error and should be corrected.");
    348 				return;
    349 			}
    350 		return true;
    351 	},
    352 	set_orderBy: function(inOrderBy) {
    353 		this.checkOrderBy(inOrderBy);
    354 		this.setOrderBy(inOrderBy);
    355 	},
    356 	makePropEdit: function(inName, inValue, inDefault) {
    357 		switch (inName) {
    358 			case "liveSource":
    359 				return new wm.propEdit.LiveSourcesSelect({component: this, name: inName, value: inValue});
    360 			case "matchMode":
    361 				return makeSelectPropEdit(inName, inValue, this._matchModes, inDefault);
    362 			case "operation":
    363 				return makeSelectPropEdit(inName, inValue, this._operations, inDefault);
    364 		}
    365 		return this.inherited(arguments);
    366 	}
    367 });
    368