Commit ecfae314 authored by Adrien's avatar Adrien Committed by Romain Bioteau
Browse files

feat(app) load layout from UI designer (#349)

Closes BS-16341
parent c3fc28ee
......@@ -44,11 +44,14 @@ public class WebPageFileStore extends InFolderJSONFileStore {
private static final String ID_TYPE = "type";
public static final String LAYOUT_TYPE = "layout";
public WebPageFileStore(final String fileName, final IRepositoryStore<? extends IRepositoryFileStore> parentStore) {
super(fileName, parentStore);
}
public void setWebFormBOSArchiveFileStoreProvider(final WebFormBOSArchiveFileStoreProvider webFormBOSArchiveFileStoreProvider) {
public void setWebFormBOSArchiveFileStoreProvider(
final WebFormBOSArchiveFileStoreProvider webFormBOSArchiveFileStoreProvider) {
this.webFormBOSArchiveFileStoreProvider = webFormBOSArchiveFileStoreProvider;
}
......@@ -94,7 +97,9 @@ public class WebPageFileStore extends InFolderJSONFileStore {
try {
return getStringAttribute(ID_TYPE);
} catch (final JSONException | ReadFileStoreException e) {
BonitaStudioLog.error(String.format("Failed to retrieve id in JSON file %s.json, with key %s.", getName(), ID_TYPE), UIDesignerPlugin.PLUGIN_ID);
BonitaStudioLog.error(
String.format("Failed to retrieve id in JSON file %s.json, with key %s.", getName(), ID_TYPE),
UIDesignerPlugin.PLUGIN_ID);
return "page";
}
}
......
......@@ -28,6 +28,7 @@ Require-Bundle: org.eclipse.core.runtime,
org.eclipse.jface.databinding,
org.bonitasoft.studio.browser,
org.bonitasoft.studio.engine;bundle-version="7.5.0",
org.bonitasoft.studio.designer,
org.bonitasoft.studio.tests-utils;bundle-version="6.4.1";resolution:=optional,
org.junit;bundle-version="4.11.0";resolution:=optional,
assertj-core;bundle-version="3.6.1";resolution:=optional,
......
......@@ -37,11 +37,12 @@ profile=Profile
saveBeforeDeployTitle=Save Resource
saveBeforeDeploy='%s' has been modified. Save changes ?
layout=Layout
layoutMessage=Your custom layouts have to be deployed manually to be used in applications
theme=Theme
versionMessage=Version of the application
descriptionMessage=Description of the application
themeMessage=You can use a custom theme created in the portal, for that insert its theme ID.
customProfile=You can manually enter a custom profile created in the portal
themeMessage=You can use a custom theme installed in the Portal, use its theme id
customProfile=You can manually enter a custom profile from the Portal
url=URL
export=Export
selectAppDescriptorToExport=Select an application descriptor to export
......@@ -70,3 +71,5 @@ importApplicationDescriptorDesc=Select a valid application descriptor xml file t
fileName=File name
invalidCharFileName='%s' is not allowed in filename
invalidFileName='%s' is not a valid filename
unknownCustomLayout=This layout does not exist in the UI Designer
unknownCustomTheme=This theme is not provided by default, make sure this theme is installed in the Portal
......@@ -29,6 +29,7 @@ import org.bonitasoft.studio.la.i18n.Messages;
import org.bonitasoft.studio.la.repository.ApplicationFileStore;
import org.bonitasoft.studio.la.repository.ApplicationRepositoryStore;
import org.bonitasoft.studio.la.ui.control.NewApplicationPage;
import org.bonitasoft.studio.la.ui.editor.layout.LayoutDescriptor;
import org.bonitasoft.studio.ui.wizard.WizardBuilder;
import org.eclipse.e4.core.di.annotations.Execute;
import org.eclipse.swt.SWT;
......@@ -37,7 +38,6 @@ import org.eclipse.swt.widgets.Shell;
public class NewApplicationHandler {
private static final String DEFAULT_PROFILE = "User";
private static final String DEFAULT_LAYOUT = "custompage_defaultlayout";
private static final String DEFAULT_THEME = "custompage_bootstrapdefaulttheme";
public static final String XML_EXTENSION = ".xml";
......@@ -52,7 +52,7 @@ public class NewApplicationHandler {
RepositoryAccessor repositoryAccessor) {
final ApplicationNode applicationNode = newApplication("myApp", "My App", "1.0").create();
applicationNode.setProfile(DEFAULT_PROFILE);
applicationNode.setLayout(DEFAULT_LAYOUT);
applicationNode.setLayout(LayoutDescriptor.DEFAULT_LAYOUT.getId());
applicationNode.setTheme(DEFAULT_THEME);
final NewApplicationPage newApplicationPage = new NewApplicationPage(applicationNode, repositoryAccessor);
......
......@@ -90,6 +90,9 @@ public class Messages extends NLS {
public static String fileName;
public static String invalidCharFileName;
public static String invalidFileName;
public static String layoutMessage;
public static String unknownCustomLayout;
public static String unknownCustomTheme;
static {
NLS.initializeMessages("messages", Messages.class);
......
......@@ -40,6 +40,7 @@ import org.bonitasoft.engine.search.SearchOptionsBuilder;
import org.bonitasoft.engine.session.APISession;
import org.bonitasoft.studio.browser.operation.OpenBrowserOperation;
import org.bonitasoft.studio.common.repository.RepositoryAccessor;
import org.bonitasoft.studio.designer.core.repository.WebPageRepositoryStore;
import org.bonitasoft.studio.engine.BOSEngineManager;
import org.bonitasoft.studio.engine.operation.GetApiSessionOperation;
import org.bonitasoft.studio.la.LivingApplicationPlugin;
......@@ -48,8 +49,12 @@ import org.bonitasoft.studio.la.core.DeployApplicationRunnable;
import org.bonitasoft.studio.la.i18n.Messages;
import org.bonitasoft.studio.la.repository.ApplicationFileStore;
import org.bonitasoft.studio.la.repository.ApplicationRepositoryStore;
import org.bonitasoft.studio.la.ui.editor.layout.LayoutDescriptor;
import org.bonitasoft.studio.la.ui.editor.layout.LayoutProvider;
import org.bonitasoft.studio.la.ui.editor.theme.ThemeDescriptor;
import org.bonitasoft.studio.la.ui.validator.ApplicationTokenUnicityValidator;
import org.bonitasoft.studio.la.ui.validator.CustomLayoutValidator;
import org.bonitasoft.studio.la.ui.validator.CustomThemeValidator;
import org.bonitasoft.studio.ui.converter.ConverterBuilder;
import org.bonitasoft.studio.ui.databinding.UpdateStrategyFactory;
import org.bonitasoft.studio.ui.dialog.ExceptionDialogHandler;
......@@ -61,19 +66,24 @@ import org.bonitasoft.studio.ui.widget.TextAreaWidget;
import org.bonitasoft.studio.ui.widget.TextWidget;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.beans.PojoObservables;
import org.eclipse.core.databinding.observable.list.WritableList;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.observable.value.ValueChangeEvent;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.databinding.viewers.ObservableListContentProvider;
import org.eclipse.jface.databinding.viewers.ViewersObservables;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.viewers.ComboViewer;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CCombo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
......@@ -95,11 +105,14 @@ public class ApplicationOverviewPage extends FormPage {
private IDocument document;
private final ApplicationNodeContainerConverter applicationNodeContainerConverter;
private final RepositoryAccessor repositoryAccessor;
private final LayoutProvider layoutProvider;
private LayoutPageListener layoutPageListener;
public ApplicationOverviewPage(String id, String title) {
super(id, title);
applicationNodeContainerConverter = new ApplicationNodeContainerConverter();
this.applicationNodeContainerConverter = new ApplicationNodeContainerConverter();
this.repositoryAccessor = repositoryAccessor();
this.layoutProvider = new LayoutProvider(repositoryAccessor.getRepositoryStore(WebPageRepositoryStore.class));
}
public void init(ApplicationNodeContainer applicationWorkingCopy, IDocument document) {
......@@ -165,7 +178,7 @@ public class ApplicationOverviewPage extends FormPage {
private URL toURL(ApplicationNode application) {
try {
return new URL("http://localhost:8080/bonita/apps/" + application.getToken()); //TODO retireve host and port from preferences
return new URL("http://localhost:8080/bonita/apps/" + application.getToken()); //TODO retrieve host and port from preferences
} catch (final MalformedURLException e) {
throw new RuntimeException(e);
}
......@@ -206,15 +219,9 @@ public class ApplicationOverviewPage extends FormPage {
final IObservableValue themeModelObservable = PojoObservables.observeValue(application, "theme");
themeModelObservable.addValueChangeListener(this::updateFile);
final Composite urlComposite = new Composite(container, SWT.NONE);
urlComposite.setLayout(GridLayoutFactory.fillDefaults().spacing(0, 0).create());
urlComposite.setLayoutData(GridDataFactory.fillDefaults().create());
final Label urlLabel = toolkit.createLabel(urlComposite, Messages.url);
urlLabel.setLayoutData(GridDataFactory.fillDefaults().create());
new TextWidget.Builder()
.withLabel("../apps/")
.withLabel(Messages.url)
.labelAbove()
.withMessage(Messages.applicationTokenMessage)
.widthHint(500)
.transactionalEdit(this::deleteAndDeployOldApp)
......@@ -234,13 +241,12 @@ public class ApplicationOverviewPage extends FormPage {
applicationTokenUnicityValidator)))
.inContext(ctx)
.adapt(toolkit)
.createIn(urlComposite)
.setLabelColor(Display.getCurrent().getSystemColor(SWT.COLOR_DARK_GRAY));
.createIn(container);
new TextWidget.Builder()
.withLabel(Messages.displayName)
.labelAbove()
.widthHint(500)
.widthHint(400)
.bindTo(nameModelObservable)
.withDelay(500)
.inContext(ctx)
......@@ -276,7 +282,7 @@ public class ApplicationOverviewPage extends FormPage {
new TextAreaWidget.Builder()
.withLabel(Messages.description)
.labelAbove()
.widthHint(500)
.widthHint(550)
.heightHint(100)
.bindTo(descriptionModelObservable)
.withDelay(500)
......@@ -286,21 +292,54 @@ public class ApplicationOverviewPage extends FormPage {
final Group lookNFeelGroup = new Group(container, SWT.NONE);
lookNFeelGroup.setLayout(GridLayoutFactory.fillDefaults().margins(10, 10).spacing(10, 10).create());
lookNFeelGroup.setLayoutData(GridDataFactory.fillDefaults().create());
lookNFeelGroup.setLayoutData(GridDataFactory.fillDefaults().hint(550, SWT.DEFAULT).create());
lookNFeelGroup.setText(Messages.lookNFeel);
final String[] layouts = { "custompage_defaultlayout" };
final MultiValidator layoutValidator = new MultiValidator.Builder().havingValidators(
new EmptyInputValidator.Builder()
.withMessage(Messages.required).create(),
new CustomLayoutValidator(layoutProvider))
.create();
new ComboWidget.Builder()
final CCombo combo = new ComboWidget.Builder()
.withLabel(Messages.layout)
.labelAbove()
.withMessage(Messages.layoutMessage)
.grabHorizontalSpace()
.fill()
.withItems(layouts)
.bindTo(layoutModelObservable)
.withModelToTargetStrategy(UpdateStrategyFactory.convertUpdateValueStrategy()
.withValidator(layoutValidator))
.withTargetToModelStrategy(UpdateStrategyFactory.convertUpdateValueStrategy()
.withValidator(layoutValidator))
.inContext(ctx)
.adapt(toolkit)
.createIn(lookNFeelGroup);
.createIn(lookNFeelGroup)
.getCombo();
final ComboViewer comboViewer = new ComboViewer(combo);
comboViewer.setContentProvider(new ObservableListContentProvider());
comboViewer.setLabelProvider(new LabelProvider());
final WritableList inputLayoutObservable = new WritableList(layoutProvider.getLayouts(), LayoutDescriptor.class);
final WebPageRepositoryStore pageStore = repositoryAccessor.getRepositoryStore(WebPageRepositoryStore.class);
layoutPageListener = new LayoutPageListener(inputLayoutObservable, pageStore);
pageStore.getResource().getWorkspace().addResourceChangeListener(layoutPageListener);
comboViewer.setInput(inputLayoutObservable);
ctx.bindValue(ViewersObservables.observeSingleSelection(comboViewer),
layoutModelObservable,
updateValueStrategy().withConverter(LayoutProvider.toLayoutId()).create(),
updateValueStrategy().withConverter(LayoutProvider.toLayoutDescriptor(inputLayoutObservable)).create());
final MultiValidator themeValidator = new MultiValidator.Builder().havingValidators(
new EmptyInputValidator.Builder()
.withMessage(Messages.required),
new CustomThemeValidator.Builder()
.withMessage(Messages.unknownCustomTheme))
.create();
new ComboWidget.Builder()
.withLabel(Messages.theme)
......@@ -315,17 +354,21 @@ public class ApplicationOverviewPage extends FormPage {
.fromType(String.class)
.toType(String.class)
.withConvertFunction(ThemeDescriptor::fromIdToName)
.create()))
.create())
.withValidator(themeValidator))
.withTargetToModelStrategy(UpdateStrategyFactory.updateValueStrategy()
.withConverter(ConverterBuilder.<String, String> newConverter()
.fromType(String.class)
.toType(String.class)
.withConvertFunction(ThemeDescriptor::fromNameToId)
.create()))
.create())
.withValidator(themeValidator))
.inContext(ctx)
.adapt(toolkit)
.createIn(lookNFeelGroup);
ctx.updateTargets();
return container;
}
......@@ -391,4 +434,17 @@ public class ApplicationOverviewPage extends FormPage {
}
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.forms.editor.FormPage#dispose()
*/
@Override
public void dispose() {
if (layoutPageListener != null) {
final WebPageRepositoryStore pageStore = repositoryAccessor.getRepositoryStore(WebPageRepositoryStore.class);
pageStore.getResource().getWorkspace().removeResourceChangeListener(layoutPageListener);
}
super.dispose();
}
}
/**
* Copyright (C) 2016 Bonitasoft S.A.
* Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
* 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 2.0 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/>.
*/
package org.bonitasoft.studio.la.ui.editor;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.bonitasoft.studio.designer.core.repository.WebPageFileStore;
import org.bonitasoft.studio.designer.core.repository.WebPageRepositoryStore;
import org.bonitasoft.studio.la.ui.editor.layout.LayoutDescriptor;
import org.bonitasoft.studio.la.ui.editor.layout.LayoutProvider;
import org.eclipse.core.databinding.observable.list.IObservableList;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.runtime.CoreException;
public class LayoutPageListener implements IResourceChangeListener {
private final IObservableList resourceList;
private final WebPageRepositoryStore store;
public LayoutPageListener(IObservableList resourceList, WebPageRepositoryStore store) {
this.resourceList = resourceList;
this.store = store;
}
/*
* (non-Javadoc)
* @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
*/
@Override
public void resourceChanged(IResourceChangeEvent event) {
try {
event.getDelta().accept(this::updateResourceList);
} catch (final CoreException e) {
throw new RuntimeException(e);
}
}
private boolean updateResourceList(IResourceDelta delta) {
final IResource resource = delta.getResource();
if ("json".equals(delta.getResource().getFileExtension())
&& store.getResource().getLocation().isPrefixOf(resource.getLocation())
&& (delta.getKind() == IResourceDelta.ADDED || delta.getKind() == IResourceDelta.REMOVED)) {
final List<LayoutDescriptor> newDescriptors = store.getChildren().stream()
.filter(fStore -> WebPageFileStore.LAYOUT_TYPE.equals(fStore.getType()))
.map(fStore -> new LayoutDescriptor(LayoutProvider.CUSTOMPAGE_PREFIX + fStore.getDisplayName(),
fStore.getDisplayName(), true))
.collect(Collectors.toList());
if (delta.getKind() == IResourceDelta.ADDED) {
resourceList.getRealm().exec(() -> resourceList.addAll(
newDescriptors.stream().filter(ld -> !resourceList.contains(ld)).collect(Collectors.toList())));
}
if (delta.getKind() == IResourceDelta.REMOVED) {
resourceList.getRealm().exec(() -> resourceList
.removeAll((Collection) resourceList.stream()
.filter(ld -> ((LayoutDescriptor) ld).isPersisted())
.filter(ld -> !newDescriptors.contains(ld))
.collect(Collectors.toList())));
}
}
return true;
}
}
/**
* Copyright (C) 2016 Bonitasoft S.A.
* Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
* 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 2.0 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/>.
*/
package org.bonitasoft.studio.la.ui.editor.layout;
import java.util.Objects;
public class LayoutDescriptor {
public static final String DEFAULT_LAYOUT_ID = "custompage_defaultlayout";
public static final LayoutDescriptor DEFAULT_LAYOUT = new LayoutDescriptor(DEFAULT_LAYOUT_ID,
"Bonita default layout",
false);
private String id;
private String displayName;
private boolean persisted;
public LayoutDescriptor(String id, String displayName, boolean persisted) {
this.id = Objects.requireNonNull(id);
this.displayName = Objects.requireNonNull(displayName);
this.persisted = persisted;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return displayName;
}
public boolean isPersisted() {
return persisted;
}
public void setPersisted(boolean persisted) {
this.persisted = persisted;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((displayName == null) ? 0 : displayName.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + (persisted ? 1231 : 1237);
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final LayoutDescriptor other = (LayoutDescriptor) obj;
if (displayName == null) {
if (other.displayName != null)
return false;
} else if (!displayName.equals(other.displayName))
return false;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (persisted != other.persisted)
return false;
return true;
}
}
/**
* Copyright (C) 2017 Bonitasoft S.A.
* Bonitasoft, 32 rue Gustave Eiffel - 38000 Grenoble
* 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 2.0 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/>.
*/
package org.bonitasoft.studio.la.ui.editor.layout;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.bonitasoft.studio.designer.core.repository.WebPageFileStore;
import org.bonitasoft.studio.designer.core.repository.WebPageRepositoryStore;
import org.bonitasoft.studio.ui.converter.ConverterBuilder;
import org.eclipse.core.databinding.conversion.IConverter;
import org.eclipse.core.databinding.observable.list.IObservableList;
public class LayoutProvider {
public static final String CUSTOMPAGE_PREFIX = "custompage_";
private final WebPageRepositoryStore store;
public LayoutProvider(WebPageRepositoryStore store) {
this.store = store;
}
public List<LayoutDescriptor> getLayouts() {
final List<LayoutDescriptor> layouts = new ArrayList<>();
layouts.add(LayoutDescriptor.DEFAULT_LAYOUT);
store.getChildren()
.stream()
.filter(webPageFileStore -> Objects.equals(webPageFileStore.getType(), WebPageFileStore.LAYOUT_TYPE))
.map(fileStore -> new LayoutDescriptor(CUSTOMPAGE_PREFIX + fileStore.getDisplayName(),
fileStore.getDisplayName(), true))
.forEach(layouts::add);
return layouts;
}
public static String fromIdToName(String id) {
return id.startsWith(CUSTOMPAGE_PREFIX) ? id.substring(CUSTOMPAGE_PREFIX.length()) : id;
}
public static String fromNameToId(String name) {