1
/*
2
 * Copyright (C) 2011 Openismus GmbH
3
 *
4
 * This file is part of GWT-Glom.
5
 *
6
 * GWT-Glom is free software: you can redistribute it and/or modify it
7
 * under the terms of the GNU Lesser General Public License as published by the
8
 * Free Software Foundation, either version 3 of the License, or (at your
9
 * option) any later version.
10
 *
11
 * GWT-Glom is distributed in the hope that it will be useful, but WITHOUT
12
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
14
 * for more details.
15
 *
16
 * You should have received a copy of the GNU Lesser General Public License
17
 * along with GWT-Glom.  If not, see <http://www.gnu.org/licenses/>.
18
 */
19
20
package org.glom.web.client.activity;
21
22
import java.util.ArrayList;
23
24
import org.glom.web.client.StringUtils;
25
import org.glom.web.client.ClientFactory;
26
import org.glom.web.client.OnlineGlomServiceAsync;
27
import org.glom.web.client.Utils;
28
import org.glom.web.client.event.LocaleChangeEvent;
29
import org.glom.web.client.event.LocaleChangeEventHandler;
30
import org.glom.web.client.event.QuickFindChangeEvent;
31
import org.glom.web.client.event.QuickFindChangeEventHandler;
32
import org.glom.web.client.event.TableChangeEvent;
33
import org.glom.web.client.event.TableChangeEventHandler;
34
import org.glom.web.client.place.DetailsPlace;
35
import org.glom.web.client.place.DocumentSelectionPlace;
36
import org.glom.web.client.place.ListPlace;
37
import org.glom.web.client.ui.DetailsView;
38
import org.glom.web.client.ui.View;
39
import org.glom.web.client.ui.cell.NavigationButtonCell;
40
import org.glom.web.client.ui.details.DetailsCell;
41
import org.glom.web.client.ui.details.Portal;
42
import org.glom.web.client.ui.details.RelatedListTable;
43
import org.glom.web.shared.DataItem;
44
import org.glom.web.shared.DetailsLayoutAndData;
45
import org.glom.web.shared.NavigationRecord;
46
import org.glom.web.shared.TypedDataItem;
47
import org.glom.web.shared.layout.LayoutGroup;
48
import org.glom.web.shared.layout.LayoutItemField;
49
import org.glom.web.shared.layout.LayoutItemPortal;
50
51
import com.google.gwt.activity.shared.AbstractActivity;
52
import com.google.gwt.cell.client.ValueUpdater;
53
import com.google.gwt.core.client.GWT;
54
import com.google.gwt.dom.client.Element;
55
import com.google.gwt.dom.client.NativeEvent;
56
import com.google.gwt.event.dom.client.ClickEvent;
57
import com.google.gwt.event.dom.client.ClickHandler;
58
import com.google.gwt.event.shared.EventBus;
59
import com.google.gwt.place.shared.Place;
60
import com.google.gwt.user.client.rpc.AsyncCallback;
61
import com.google.gwt.user.client.ui.AcceptsOneWidget;
62
63
/**
64
 *
65
 */
66
public class DetailsActivity extends AbstractActivity implements View.Presenter {
67
	/*
68
	 * Cell renderer for the related list open buttons. Normally this wouldn't be in an Activity class but since it's
69
	 * making a call to the server it makes sense for it to be here.
70
	 */
71
	private class RelatedListNavigationButtonCell extends NavigationButtonCell {
72
73
		private final String relationshipName;
74
75
		public RelatedListNavigationButtonCell(final String relationshipName) {
76
			this.relationshipName = relationshipName;
77
		}
78
79
		/*
80
		 * (non-Javadoc)
81
		 * 
82
		 * @see com.google.gwt.cell.client.ButtonCell#onEnterKeyDown(com.google.gwt.cell.client.Cell.Context,
83
		 * com.google.gwt.dom.client.Element, java.lang.String, com.google.gwt.dom.client.NativeEvent,
84
		 * com.google.gwt.cell.client.ValueUpdater)
85
		 */
86
		@Override
87
		protected void onEnterKeyDown(final Context context, final Element parent, final String value,
88
				final NativeEvent event, final ValueUpdater<String> valueUpdater) {
89
			final AsyncCallback<NavigationRecord> callback = new AsyncCallback<NavigationRecord>() {
90
				@Override
91
				public void onFailure(final Throwable caught) {
92
					// TODO: create a way to notify users of asynchronous callback failures
93
					GWT.log("AsyncCallback Failed: OnlineGlomService.getSuitableRecordToViewDetails()");
94
				}
95
96
				@Override
97
				public void onSuccess(final NavigationRecord result) {
98
					processNavigation(result.getTableName(), result.getPrimaryKeyValue());
99
				}
100
101
			};
102
			OnlineGlomServiceAsync.Util.getInstance().getSuitableRecordToViewDetails(documentID, tableName,
103
					relationshipName, (TypedDataItem) context.getKey(), callback);
104
		}
105
	}
106
107
	private String documentID = "";
108
	private String tableName = "";
109
	private TypedDataItem primaryKeyValue;
110
	private final ClientFactory clientFactory;
111
	private final DetailsView detailsView;
112
	ArrayList<DetailsCell> detailsCells;
113
	ArrayList<Portal> portals;
114
115
	public DetailsActivity(final DetailsPlace place, final ClientFactory clientFactory) {
116
		this.documentID = place.getDocumentID();
117
		this.tableName = place.getTableName();
118
		this.primaryKeyValue = place.getPrimaryKeyValue();
119
		this.clientFactory = clientFactory;
120
		detailsView = clientFactory.getDetailsView();
121
	}
122
123
	/*
124
	 * (non-Javadoc)
125
	 * 
126
	 * @see com.google.gwt.activity.shared.Activity#start(com.google.gwt.user.client.ui.AcceptsOneWidget,
127
	 * com.google.gwt.event.shared.EventBus)
128
	 */
129
	@Override
130
	public void start(final AcceptsOneWidget panel, final EventBus eventBus) {
131
		if (StringUtils.isEmpty(documentID))
132
			goTo(new DocumentSelectionPlace());
133
134
		// register this class as the presenter
135
		detailsView.setPresenter(this);
136
137
		// TODO here's where we should check for database authentication - see ListActivity.start() for how to do this
138
139
		// set the change handler for the table selection widget
140
		eventBus.addHandler(TableChangeEvent.TYPE, new TableChangeEventHandler() {
141
			@Override
142
			public void onTableChange(final TableChangeEvent event) {
143
				// note the empty primary key item
144
				goTo(new DetailsPlace(documentID, event.getNewTableName(), new TypedDataItem()));
145
			}
146
		});
147
148
		// get the layout and data for the DetailsView
149
		final AsyncCallback<DetailsLayoutAndData> callback = new AsyncCallback<DetailsLayoutAndData>() {
150
			@Override
151
			public void onFailure(final Throwable caught) {
152
				// TODO: create a way to notify users of asynchronous callback failures
153
				GWT.log("AsyncCallback Failed: OnlineGlomService.getDetailsLayoutAndData()");
154
			}
155
156
			@Override
157
			public void onSuccess(final DetailsLayoutAndData result) {
158
				if (result == null) {
159
					// The result is null only when the documentID was not found. There's nothing to display without the
160
					// documentID.
161
					goTo(new DocumentSelectionPlace());
162
				} else {
163
					// create the layout and set the data
164
					createLayout(result.getLayout());
165
					setData(result.getData());
166
				}
167
			}
168
169
		};
170
		
171
		final String localeID = Utils.getCurrentLocaleID();
172
		OnlineGlomServiceAsync.Util.getInstance().getDetailsLayoutAndData(documentID, tableName, primaryKeyValue,
173
				localeID, callback);
174
175
		// set the change handler for the quickfind text widget
176
		eventBus.addHandler(QuickFindChangeEvent.TYPE, new QuickFindChangeEventHandler() {
177
			@Override
178
			public void onQuickFindChange(final QuickFindChangeEvent event) {
179
				// We switch to the List view, to show search results.
180
				// TODO: Show the details view if there is only one result.
181
				goTo(new ListPlace(documentID, tableName, event.getNewQuickFindText()));
182
			}
183
		});
184
185
		// Set the change handler for the table selection widget
186
		eventBus.addHandler(LocaleChangeEvent.TYPE, new LocaleChangeEventHandler() {
187
			@Override
188
			public void onLocaleChange(final LocaleChangeEvent event) {
189
				// note the empty primary key item
190
				goTo(new DetailsPlace(documentID, tableName, primaryKeyValue));
191
			}
192
		});
193
194
		// indicate that the view is ready to be displayed
195
		panel.setWidget(detailsView.asWidget());
196
	}
197
198
	/*
199
	 * Create the layout.
200
	 */
201
	private void createLayout(final ArrayList<LayoutGroup> layout) {
202
		// add the groups
203
		for (final LayoutGroup layoutGroup : layout) {
204
			detailsView.addGroup(layoutGroup);
205
		}
206
207
		// save references to the DetailsCells and the Portals
208
		detailsCells = detailsView.getCells();
209
		portals = detailsView.getPortals();
210
211
		// Setup click handlers for the navigation buttons
212
		for (final DetailsCell detailsCell : detailsCells) {
213
			final LayoutItemField layoutItemField = detailsCell.getLayoutItemField();
214
			if (layoutItemField.getAddNavigation()) {
215
				detailsCell.setOpenButtonClickHandler(new ClickHandler() {
216
					@Override
217
					public void onClick(final ClickEvent event) {
218
						final TypedDataItem primaryKeyItem = Utils.getTypedDataItem(layoutItemField.getType(),
219
								detailsCell.getData());
220
						processNavigation(layoutItemField.getNavigationTableName(), primaryKeyItem);
221
222
					}
223
				});
224
			}
225
		}
226
227
	}
228
229
	/*
230
	 * Set the data.
231
	 */
232
	private void setData(final DataItem[] data) {
233
234
		if (data == null)
235
			return;
236
237
		// TODO create proper client side logging
238
		if (data.length != detailsCells.size())
239
			GWT.log("Warning: The number of data items doesn't match the number of data detailsCells.");
240
241
		for (int i = 0; i < Math.min(detailsCells.size(), data.length); i++) {
242
			final DetailsCell detailsCell = detailsCells.get(i);
243
			if (data[i] != null) {
244
245
				// set the DatailsItem
246
				detailsCell.setData(data[i]);
247
248
				// see if there are any related lists that need to be setup
249
				for (final Portal portal : portals) {
250
					final LayoutItemField layoutItemField = detailsCell.getLayoutItemField();
251
					final LayoutItemPortal layoutItemPortal = portal.getLayoutItem();
252
253
					if (layoutItemField.getName().equals(layoutItemPortal.getFromField())) {
254
						if (data[i] == null)
255
							continue;
256
257
						final TypedDataItem foreignKeyValue = Utils
258
								.getTypedDataItem(layoutItemField.getType(), data[i]);
259
260
						final RelatedListTable relatedListTable = new RelatedListTable(documentID, layoutItemPortal,
261
								foreignKeyValue, new RelatedListNavigationButtonCell(layoutItemPortal.getName()));
262
263
						if (!layoutItemPortal.getAddNavigation()
264
								|| layoutItemPortal.getNavigationType() == LayoutItemPortal.NavigationType.NAVIGATION_NONE) {
265
							relatedListTable.hideNavigationButtons();
266
						}
267
						portal.setContents(relatedListTable);
268
269
						setRowCountForRelatedListTable(relatedListTable, layoutItemPortal.getName(), foreignKeyValue);
270
					}
271
				}
272
			}
273
		}
274
	}
275
276
	private void refreshData() {
277
278
		// get the data for the DetailsView
279
		final AsyncCallback<DataItem[]> callback = new AsyncCallback<DataItem[]>() {
280
			@Override
281
			public void onFailure(final Throwable caught) {
282
				// TODO: create a way to notify users of asynchronous callback failures
283
				GWT.log("AsyncCallback Failed: OnlineGlomService.getDetailsData()");
284
			}
285
286
			@Override
287
			public void onSuccess(final DataItem[] result) {
288
				setData(result);
289
			}
290
		};
291
292
		OnlineGlomServiceAsync.Util.getInstance().getDetailsData(documentID, tableName, primaryKeyValue, callback);
293
294
	}
295
296
	// sets the row count for the related list table
297
	private void setRowCountForRelatedListTable(final RelatedListTable relatedListTable, final String relationshipName,
298
			final TypedDataItem foreignKeyValue) {
299
		final AsyncCallback<Integer> callback = new AsyncCallback<Integer>() {
300
			@Override
301
			public void onFailure(final Throwable caught) {
302
				// TODO: create a way to notify users of asynchronous callback failures
303
				GWT.log("AsyncCallback Failed: OnlineGlomService.getRelatedListRowCount()");
304
			}
305
306
			@Override
307
			public void onSuccess(final Integer result) {
308
				if (result.intValue() <= relatedListTable.getMinNumVisibleRows()) {
309
					// Set the table row count to the minimum row count if the data row count is less than or equal to
310
					// the minimum row count. This ensures that data with fewer rows than the minimum will not create
311
					// indexes in the underlying CellTable that will override the rendering of the empty rows.
312
					relatedListTable.setRowCount(relatedListTable.getMinNumVisibleRows());
313
				} else {
314
					// Set the table row count to the data row count if it's larger than the minimum number of rows
315
					// visible.
316
					relatedListTable.setRowCount(result.intValue());
317
				}
318
			}
319
		};
320
321
		OnlineGlomServiceAsync.Util.getInstance().getRelatedListRowCount(documentID, tableName, relationshipName,
322
				foreignKeyValue, callback);
323
	}
324
325
	/*
326
	 * Process a navigation by either doing: nothing if the navigation isn't valid, refreshing the data for the current
327
	 * table with a new primary key, or going to a new table with a new primary key.
328
	 */
329
	private void processNavigation(final String navigationTableName, final TypedDataItem navigationPrimaryKeyValue) {
330
331
		// Ensure the new table name is valid.
332
		String newTableName;
333
		if (!StringUtils.isEmpty(navigationTableName)) {
334
			newTableName = navigationTableName;
335
		} else {
336
			newTableName = tableName;
337
		}
338
339
		// Only process the navigation if there's a valid primary key value.
340
		if (navigationPrimaryKeyValue != null && !navigationPrimaryKeyValue.isEmpty()) {
341
			if (!newTableName.equals(tableName)) {
342
				// Go to a new DetailsPlace because the table name has changed.
343
				goTo(new DetailsPlace(documentID, newTableName, navigationPrimaryKeyValue));
344
			} else {
345
				// Refresh the details view with the new primary because the table name has not changed.
346
				primaryKeyValue = navigationPrimaryKeyValue;
347
				refreshData();
348
			}
349
		} else {
350
			// TODO notify the user that navigation isn't possible.
351
			// This is what Glom displays:
352
			// Frame_Glom::show_ok_dialog(_("No Corresponding Record Exists"),
353
			// _("No record with this value exists. Therefore navigation to the related record is not possible."),
354
			// *window, Gtk::MESSAGE_WARNING);
355
			// TODO: Make it more clear to the user exactly what record, what field, and what value, we are talking
356
			// about.
357
		}
358
	}
359
360
	/*
361
	 * (non-Javadoc)
362
	 * 
363
	 * @see com.google.gwt.activity.shared.Activity#onCancel()
364
	 */
365
	@Override
366
	public void onCancel() {
367
		detailsView.clear();
368
	}
369
370
	/*
371
	 * (non-Javadoc)
372
	 * 
373
	 * @see com.google.gwt.activity.shared.Activity#onStop()
374
	 */
375
	@Override
376
	public void onStop() {
377
		detailsView.clear();
378
	}
379
380
	/*
381
	 * (non-Javadoc)
382
	 * 
383
	 * @see org.glom.web.client.ui.View.Presenter#goTo(com.google.gwt.place.shared.Place)
384
	 */
385
	@Override
386
	public void goTo(final Place place) {
387
		clientFactory.getPlaceController().goTo(place);
388
	}
389
390
}