package org.sigmah.client.ui.view.project.indicator;
/*
* #%L
* Sigmah
* %%
* Copyright (C) 2010 - 2016 URD
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.sigmah.client.event.SiteEvent;
import org.sigmah.client.i18n.I18N;
import org.sigmah.shared.command.Delete;
import org.sigmah.shared.command.GetSites;
import org.sigmah.shared.command.result.PagingResult;
import org.sigmah.shared.command.result.SiteResult;
import org.sigmah.shared.command.result.VoidResult;
import org.sigmah.shared.dto.IndicatorDTO;
import org.sigmah.shared.dto.SchemaDTO;
import org.sigmah.shared.dto.SiteDTO;
import com.allen_sauer.gwt.log.client.Log;
import com.extjs.gxt.ui.client.data.BasePagingLoader;
import com.extjs.gxt.ui.client.data.LoadEvent;
import com.extjs.gxt.ui.client.data.PagingLoadConfig;
import com.extjs.gxt.ui.client.data.PagingLoader;
import com.extjs.gxt.ui.client.data.RpcProxy;
import com.extjs.gxt.ui.client.event.ButtonEvent;
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.GridEvent;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.event.LoadListener;
import com.extjs.gxt.ui.client.event.MenuEvent;
import com.extjs.gxt.ui.client.event.SelectionChangedEvent;
import com.extjs.gxt.ui.client.event.SelectionChangedListener;
import com.extjs.gxt.ui.client.event.SelectionListener;
import com.extjs.gxt.ui.client.event.SelectionProvider;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.store.Record;
import com.extjs.gxt.ui.client.widget.button.Button;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.Label;
import com.extjs.gxt.ui.client.widget.grid.ColumnModel;
import com.extjs.gxt.ui.client.widget.grid.EditorGrid;
import com.extjs.gxt.ui.client.widget.grid.EditorGrid.ClicksToEdit;
import com.extjs.gxt.ui.client.widget.grid.GridSelectionModel;
import com.extjs.gxt.ui.client.widget.layout.FitLayout;
import com.extjs.gxt.ui.client.widget.menu.MenuItem;
import com.extjs.gxt.ui.client.widget.toolbar.PagingToolBar;
import com.extjs.gxt.ui.client.widget.toolbar.SeparatorToolItem;
import com.extjs.gxt.ui.client.widget.toolbar.ToolBar;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;
import org.sigmah.client.dispatch.CommandResultHandler;
import org.sigmah.client.dispatch.DispatchAsync;
import org.sigmah.client.dispatch.monitor.LoadingMask;
import org.sigmah.client.event.EventBus;
import org.sigmah.client.event.handler.SiteHandler;
import org.sigmah.client.security.AuthenticationProvider;
import org.sigmah.client.ui.notif.ConfirmCallback;
import org.sigmah.client.ui.notif.N10N;
import org.sigmah.client.ui.presenter.project.indicator.SavingHelper;
import org.sigmah.client.ui.res.icon.IconImageBundle;
import org.sigmah.client.ui.widget.button.SplitButton;
import org.sigmah.client.ui.widget.form.Forms;
import org.sigmah.client.ui.widget.toolbar.ActionToolBar;
import org.sigmah.shared.command.result.ListResult;
import org.sigmah.shared.command.result.Result;
import org.sigmah.shared.dto.referential.GlobalPermissionEnum;
import org.sigmah.shared.util.ProfileUtils;
import org.sigmah.shared.util.Filter;
/**
* Component that provides for listing and editing Site objects.
*
* @author Alexander Bertram (akbertram@gmail.com)
* @author Raphaƫl Calabro (rcalabro@ideia.fr) v2.0
*/
public class SiteGridPanel extends ContentPanel implements SelectionProvider<SiteDTO> {
private HandlerRegistration handlerRegistration;
public static final int PAGE_SIZE = 25;
@Inject
private EventBus eventBus;
@Inject
private DispatchAsync service;
@Inject
private AuthenticationProvider authenticationProvider;
private PagingLoader<SiteResult> loader;
private final ListStore<SiteDTO> store;
private EditorGrid<SiteDTO> grid;
private ToolBar toolBar;
private SplitButton saveButton;
private Button manageMainSiteButton;
private Button newSiteButton;
private Button editButton;
private Button deleteButton;
private Filter filter;
private Integer siteIdToSelectOnNextLoad;
private boolean siteUpdated;
private Integer mainSiteId;
private SchemaDTO schema;
public SiteGridPanel() {
setLayout(new FitLayout());
setHeadingText(I18N.CONSTANTS.sites());
initLoader();
store = new ListStore<SiteDTO>(loader);
initToolBar();
initPagingToolBar();
}
public void initialize() {
handlerRegistration = eventBus.addHandler(SiteEvent.getType(), new SiteHandler() {
@Override
public void handleEvent(SiteEvent siteEvent) {
switch(siteEvent.getAction()) {
case CREATED:
onSiteCreated(siteEvent);
break;
case UPDATED:
onSiteUpdated(siteEvent);
break;
case DELETED:
onSiteDeleted(siteEvent);
break;
case MAIN_SITE_CREATED:
onMainSiteCreated(siteEvent);
break;
case MAIN_SITE_UPDATED:
onMainSiteUpdated(siteEvent);
break;
}
}
});
}
/**
* Indicates if a site was updated
*
* @return true if a site was updated
*/
public boolean isSiteUpdated() {
return siteUpdated;
}
/**
* Set the variable siteUpdated
*
* @param siteUpdated
* the new value of siteUpdated
*/
public void setSiteUpdated(boolean siteUpdated) {
this.siteUpdated = siteUpdated;
}
public void setSchema(SchemaDTO schema) {
this.schema = schema;
}
private void initLoader() {
loader = new BasePagingLoader<SiteResult>(new SiteProxy());
loader.addLoadListener(new LoadListener() {
@Override
public void loaderBeforeLoad(LoadEvent le) {
if (grid != null) {
grid.mask();
}
}
@Override
public void loaderLoad(LoadEvent le) {
// set the first item as selected
if (store.getCount() > 0) {
grid.getSelectionModel().setSelection(Collections.singletonList(store.getAt(0)));
}
if (grid != null) {
grid.unmask();
}
}
@Override
public void loaderLoadException(LoadEvent le) {
Log.debug("SiteGridPanel load failed", le.exception);
if (grid == null) {
removeAll();
add(new Label(I18N.CONSTANTS.connectionProblem()));
layout();
} else {
grid.getView().setEmptyText(I18N.CONSTANTS.connectionProblem());
grid.unmask();
}
store.removeAll();
}
});
}
/**
* Loads the sites using the given filter and with the given mainSite.
* This must be called by the container.
*
* @param filter
* @param mainSiteId
*/
public void load(Filter filter, Integer mainSiteId) {
this.mainSiteId = mainSiteId;
manageMainSiteButton.setEnabled(ProfileUtils.isGranted(authenticationProvider.get(), GlobalPermissionEnum.MANAGE_MAIN_SITE));
// Disable creating sites until a main site is created.
newSiteButton.setEnabled(canManageSites() && mainSiteId != null);
// By default the label is "Create". If the project has a main site, use
// the "Edit" label for this button.
manageMainSiteButton.setText(mainSiteId != null ?
I18N.CONSTANTS.editMainSiteButton() : I18N.CONSTANTS.createMainSiteButton());
load(filter, Collections.<IndicatorDTO> emptySet());
}
/**
* Loads the sites using the given filter, and including columns for the supplied indicators. This must be called by
* the container.
*
* @param filter
* @param indicators
* a list of indicators to include as columns
*/
private void load(Filter filter, Collection<IndicatorDTO> indicators) {
this.filter = filter;
this.siteIdToSelectOnNextLoad = 0;
loader.setOffset(0);
loader.load();
final SiteColumnModelBuilder siteColumnModelBuilder = new SiteColumnModelBuilder(schema, filter, this.mainSiteId, indicators, new AsyncCallback<ColumnModel>() {
@Override
public void onSuccess(ColumnModel result) {
initGrid(result);
}
@Override
public void onFailure(Throwable caught) {
// TODO: Write a more specific error message.
N10N.error(I18N.CONSTANTS.errorOnServer());
}
});
siteColumnModelBuilder.buildColumnModel();
}
/**
* Clear the content of the grid.
*/
public void clear() {
store.removeAll();
editButton.setEnabled(false);
deleteButton.setEnabled(false);
}
/**
* Gets this grid's ActionToolBar. Consumers of this widget can add additional buttons.
*
* @return this grid's {@link ActionToolBar}
*/
public ToolBar getToolBar() {
return toolBar;
}
public Button getNewSiteButton() {
return newSiteButton;
}
public Button getManageMainSiteButton() {
return manageMainSiteButton;
}
public Button getEditButton() {
return editButton;
}
/**
* Creates the {@link ActionToolBar} used as the top component. This can be overriden by subclasses.
*
* @return the toolbar
*/
private void initToolBar() {
toolBar = new ToolBar();
// Save button.
saveButton = Forms.saveSplitButton();
final MenuItem saveItem = (MenuItem) saveButton.getMenu().getItem(0);
final MenuItem discardChangesItem = (MenuItem) saveButton.getMenu().getItem(1);
saveButton.addSelectionListener(new SelectionListener<ButtonEvent>() {
@Override
public void componentSelected(ButtonEvent ce) {
save();
}
});
saveItem.addSelectionListener(new SelectionListener<MenuEvent>() {
@Override
public void componentSelected(MenuEvent ce) {
save();
}
});
discardChangesItem.addSelectionListener(new SelectionListener<MenuEvent>() {
@Override
public void componentSelected(MenuEvent ce) {
grid.getStore().rejectChanges();
}
});
// Manage main site button.
manageMainSiteButton = Forms.button(I18N.CONSTANTS.createMainSiteButton(), IconImageBundle.ICONS.mainSite());
manageMainSiteButton.addSelectionListener(new SelectionListener<ButtonEvent>() {
@Override
public void componentSelected(ButtonEvent ce) {
autoSelectMainSite();
}
});
// New site button.
newSiteButton = Forms.button(I18N.CONSTANTS.newSite(), IconImageBundle.ICONS.add());
// Edit button.
editButton = Forms.button(I18N.CONSTANTS.edit(), IconImageBundle.ICONS.editPage());
// Delete button.
deleteButton = Forms.button(I18N.CONSTANTS.deleteSite(), IconImageBundle.ICONS.delete());
deleteButton.addSelectionListener(new SelectionListener<ButtonEvent>() {
@Override
public void componentSelected(ButtonEvent ce) {
delete();
}
});
// Adding the buttons to the toolbar.
toolBar.add(saveButton);
toolBar.add(new SeparatorToolItem());
toolBar.add(manageMainSiteButton);
toolBar.add(new SeparatorToolItem());
toolBar.add(newSiteButton);
toolBar.add(editButton);
toolBar.add(deleteButton);
setTopComponent(toolBar);
}
private void initPagingToolBar() {
PagingToolBar bar = new PagingToolBar(PAGE_SIZE);
bar.bind(loader);
setBottomComponent(bar);
}
@Override
public List<SiteDTO> getSelection() {
return grid == null ? Collections.<SiteDTO> emptyList() : grid.getSelectionModel().getSelection();
}
@Override
public void addSelectionChangedListener(SelectionChangedListener<SiteDTO> listener) {
addListener(Events.SelectionChange, listener);
}
@Override
public void removeSelectionListener(SelectionChangedListener<SiteDTO> listener) {
removeListener(Events.SelectionChange, listener);
}
public void addActionListener(Listener<ComponentEvent> listener) {
toolBar.addListener(Events.Select, listener);
}
@Override
public void setSelection(List<SiteDTO> selection) {
if (grid != null) {
grid.getSelectionModel().setSelection(selection);
}
}
/**
* A GXT grid can't be created without a column model, and since our column model is a function of the filter
* applied, we delay creating the control until load() is called above.
*
* @param model
*/
private void initGrid(ColumnModel model) {
if (grid == null) {
grid = new EditorGrid<SiteDTO>(store, model);
grid.setClicksToEdit(ClicksToEdit.TWO);
grid.addListener(Events.AfterEdit, new Listener<GridEvent<SiteDTO>>() {
@Override
public void handleEvent(GridEvent<SiteDTO> event) {
siteUpdated = true;
saveButton.setEnabled(true);
}
});
grid.setSelectionModel(new GridSelectionModel<SiteDTO>());
grid.getSelectionModel().addSelectionChangedListener(new SelectionChangedListener<SiteDTO>() {
@Override
public void selectionChanged(SelectionChangedEvent<SiteDTO> se) {
onSelectionChanged(se);
}
});
removeAll();
add(grid);
layout();
} else {
grid.reconfigure(store, model);
}
}
private void save() {
// defensive copy, this may change while we're saving
final List<Record> dirty = new ArrayList<Record>(store.getModifiedRecords());
service.execute(SavingHelper.createUpdateCommand(store), new CommandResultHandler<ListResult<Result>>() {
@Override
protected void onCommandFailure(Throwable caught) {
siteUpdated = true;
saveButton.setEnabled(true);
}
@Override
protected void onCommandSuccess(ListResult<Result> result) {
siteUpdated = false;
saveButton.setEnabled(false);
for (Record record : dirty) {
record.commit(false);
}
for (Record record : dirty) {
eventBus.fireEvent(new SiteEvent(SiteEvent.Action.UPDATED, SiteGridPanel.this, (SiteDTO) record.getModel()));
}
}
});
}
private void delete() {
final SiteDTO site = grid.getSelectionModel().getSelectedItem();
N10N.confirmation(I18N.CONSTANTS.deleteSite(), I18N.MESSAGES.confirmDelete(site.getLocationName()), new ConfirmCallback() {
@Override
public void onAction() {
service.execute(new Delete(site),
new AsyncCallback<VoidResult>() {
@Override
public void onFailure(Throwable caught) {
// handled by monitor
}
@Override
public void onSuccess(VoidResult result) {
store.remove(site);
eventBus.fireEvent(new SiteEvent(SiteEvent.Action.DELETED, SiteGridPanel.this, site));
}
}, new LoadingMask(grid, I18N.CONSTANTS.deleting()));
}
});
}
private void autoSelectMainSite() {
if (this.mainSiteId != null) {
SiteDTO model = store.findModel("id", this.mainSiteId);
if (model != null) {
grid.getSelectionModel().setSelection(Collections.singletonList(model));
}
}
}
private void onSiteCreated(SiteEvent se) {
if (store.getCount() < PAGE_SIZE) {
// there is only one page, so we can save some time by justing adding this model to directly to
// the store
store.add(se.getEntity());
} else {
// there are multiple pages and we don't really know where this site is going
// to end up, so do a reload and seek to the page with the new site
siteIdToSelectOnNextLoad = se.getEntity().getId();
loader.load();
}
}
private void onSiteUpdated(SiteEvent se) {
SiteDTO site = store.findModel("id", se.getSiteId());
if (site != null) {
site.setProperties(se.getEntity().getProperties());
store.update(site);
}
}
private void onSiteDeleted(SiteEvent se) {
SiteDTO site = store.findModel("id", se.getSiteId());
if (site != null) {
store.remove(site);
}
}
private void onMainSiteCreated(SiteEvent se) {
this.mainSiteId = se.getEntity().getId();
// reload the grid
load(filter, mainSiteId);
// By default the label is "Create". If the user creates a main site, use
// the "Edit" label for this button.
if (mainSiteId != null) {
manageMainSiteButton.setText(I18N.CONSTANTS.editMainSiteButton());
newSiteButton.setEnabled(canManageSites());
}
onSiteCreated(se);
}
private void onMainSiteUpdated(SiteEvent se) {
this.mainSiteId = se.getEntity().getId();
onSiteUpdated(se);
}
public void shutdown() {
handlerRegistration.removeHandler();
}
protected void onLoaded(LoadEvent le) {
PagingResult result = (PagingResult) le.getData();
// toolBar.setActionEnabled(UIActions.export, result.getTotalLength() != 0);
if (siteIdToSelectOnNextLoad != null) {
SiteDTO model = store.findModel("id", siteIdToSelectOnNextLoad);
if (model != null) {
grid.getSelectionModel().setSelection(Collections.singletonList(model));
}
siteIdToSelectOnNextLoad = null;
}
}
public void onSelectionChanged(SelectionChangedEvent<SiteDTO> event) {
// Enable toolbar buttons for selection
deleteButton.setEnabled(false);
editButton.setEnabled(false);
if (!event.getSelection().isEmpty()) {
// Disable the toolbar for the main site
// It should be handled only through the Set main site button
// from project > map
if (this.mainSiteId == null
|| !this.mainSiteId.equals(event.getSelectedItem().getId())) {
// Not the main site, check editable
final boolean canManageSites = canManageSites();
deleteButton.setEnabled(canManageSites);
editButton.setEnabled(canManageSites);
}
}
fireEvent(Events.SelectionChange, event);
}
private class SiteProxy extends RpcProxy<SiteResult> {
@Override
protected void load(Object loadConfig, AsyncCallback<SiteResult> callback) {
PagingLoadConfig config = (PagingLoadConfig) loadConfig;
GetSites cmd = new GetSites();
cmd.setLimit(config.getLimit());
cmd.setOffset(config.getOffset());
cmd.setFilter(filter);
cmd.setSeekToSiteId(siteIdToSelectOnNextLoad);
service.execute(cmd, callback);
}
}
private boolean canManageSites() {
return (ProfileUtils.isGranted(authenticationProvider.get(), GlobalPermissionEnum.MANAGE_SITES));
}
}