View Javadoc
1   /**
2    * This file Copyright (c) 2018-2021 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.editor;
35  
36  import static java.util.stream.Collectors.toList;
37  
38  import info.magnolia.i18nsystem.SimpleTranslator;
39  import info.magnolia.icons.MagnoliaIcons;
40  import info.magnolia.ui.contentapp.Datasource;
41  import info.magnolia.ui.framework.WithImplementation;
42  import info.magnolia.ui.theme.ResurfaceTheme;
43  
44  import java.util.ArrayList;
45  import java.util.Collections;
46  import java.util.LinkedHashMap;
47  import java.util.List;
48  import java.util.Locale;
49  import java.util.Map;
50  import java.util.concurrent.CompletableFuture;
51  import java.util.stream.Stream;
52  
53  import javax.inject.Inject;
54  
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  
58  import com.vaadin.data.BinderValidationStatus;
59  import com.vaadin.ui.AbstractOrderedLayout;
60  import com.vaadin.ui.Alignment;
61  import com.vaadin.ui.Button;
62  import com.vaadin.ui.Component;
63  import com.vaadin.ui.HorizontalLayout;
64  import com.vaadin.ui.Notification;
65  import com.vaadin.ui.VerticalLayout;
66  
67  import lombok.EqualsAndHashCode;
68  import lombok.Getter;
69  import lombok.Setter;
70  
71  /**
72   * Editor view which hosts a number of similar child editors.
73   *
74   * @param <T>
75   *     item type.
76   */
77  public class MultiFormView<T> implements EditorView<T> {
78  
79      private static final Logger log = LoggerFactory.getLogger(MultiFormView.class);
80  
81      private final MultiFormDefinition<T> definition;
82      private final Locale locale;
83      private final Datasource<T> datasource;
84      private final MultiFormState state = new MultiFormState();
85  
86      private VerticalLayout rootLayout = new VerticalLayout();
87  
88      @Inject
89      public MultiFormView(MultiFormDefinition<T> definition, LocaleContext localeContext, Datasource<T> datasource) {
90          this.definition = definition;
91          this.locale = localeContext.getDefault();
92          this.datasource = datasource;
93          this.state.entryResolution = create(definition.getEntryResolution(), this.locale);
94          this.rootLayout.setSpacing(true);
95          this.rootLayout.addStyleName("multi-form-view");
96  
97          layout();
98      }
99  
100     /**
101      * @deprecated since 6.2.3. please use {@link #MultiFormView(MultiFormDefinition, LocaleContext, Datasource)} instead.
102      */
103     @Deprecated
104     public MultiFormView(MultiFormDefinition<T> definition, SimpleTranslator i18n, LocaleContext localeContext, Datasource<T> datasource) {
105         this(definition, localeContext, datasource);
106     }
107 
108     @Override
109     public List<BinderValidationStatus<?>> validate() {
110         return getAllChildren()
111                 .flatMap(view -> view.validate().stream())
112                 .collect(toList());
113     }
114 
115     @Override
116     public void write(T item) {
117         Map<EditorView<T>, ItemProviderStrategy<T, T>> children = state.children;
118         state.removedItemAccessors.stream()
119                 .map(accessor -> accessor.read(item, locale))
120                 .forEach(itemToMaybeDelete ->
121                         itemToMaybeDelete
122                                 .ifPresent(this.datasource::remove));
123 
124         List<T> itemOrder = new ArrayList<>();
125         state.order.forEach(subForm -> writeSubForm(item, children, itemOrder, subForm));
126 
127         create(this.definition.getOrderHandler(), locale).applyOrder(itemOrder);
128     }
129 
130     protected void writeSubForm(T item, Map<EditorView<T>, ItemProviderStrategy<T, T>> children, List<T> itemOrder, EditorView<T> subForm) {
131         children.get(subForm).read(item, locale).ifPresent(localisedItem -> {
132             itemOrder.add(localisedItem);
133             subForm.write(localisedItem);
134         });
135     }
136 
137     @Override
138     public Component asVaadinComponent() {
139         return rootLayout;
140     }
141 
142     @Override
143     public void populate(T item) {
144         getAllChildren().forEach(subForm -> {
145            rootLayout.removeComponent(subForm.asVaadinComponent());
146            subForm.destroy();
147         });
148 
149         state.clear();
150         populateSubNodes(item);
151         layout();
152     }
153 
154     protected void populateSubNodes(T item) {
155         state.entryResolution.resolveForRoot(item).forEach(propertyDefinition -> {
156             if (propertyDefinition != null) {
157                 EditorView<T> subForm = createSubForm(propertyDefinition);
158                 addSubFormToState(subForm, propertyDefinition);
159                 populate(item, subForm);
160             }
161         });
162     }
163 
164     protected EditorView<T> createSubForm(ComplexPropertyDefinition<T> propertyDefinition) {
165         final EditorView<T> subForm = create("child", propertyDefinition.getEditorDefinition());
166         if (subForm.asVaadinComponent().getCaption() == null) {
167             subForm.asVaadinComponent().setCaption(propertyDefinition.getLabel());
168         }
169         return subForm;
170     }
171 
172     protected void addSubFormToState(EditorView<T> subForm, ComplexPropertyDefinition<T> propertyDefinition) {
173         ItemProviderStrategy<T, T> itemProviderStrategy = create(propertyDefinition.getItemProvider());
174         state.children.put(subForm, itemProviderStrategy);
175         state.order.add(subForm);
176     }
177 
178     private void populate(T item, EditorView<T> subForm) {
179         state.children.get(subForm).read(item, locale).ifPresent(subForm::populate);
180     }
181 
182     public void layout() {
183         rootLayout.removeAllComponents();
184         rootLayout.setMargin(false);
185         state.children.keySet().stream()
186                 .map(this::wrapChildForm)
187                 .forEach(this.rootLayout::addComponent);
188         attachAddButton();
189     }
190 
191     protected Component wrapChildForm(EditorView<T> subForm) {
192         HorizontalLayout wrapLayout = new HorizontalLayout();
193         wrapLayout.setWidth("100%");
194         subForm.asVaadinComponent().addStyleName("multi-form-entry-content");
195 
196         HorizontalLayout buttonLayout = createButtonsLayout(subForm, wrapLayout);
197         wrapLayout.addComponents(subForm.asVaadinComponent(), buttonLayout);
198         wrapLayout.setExpandRatio(subForm.asVaadinComponent(), 1f);
199         wrapLayout.setComponentAlignment(buttonLayout, Alignment.TOP_CENTER);
200         wrapLayout.addStyleName("multi-form-entry");
201 
202         return wrapLayout;
203     }
204 
205     protected HorizontalLayout createButtonsLayout(EditorView<T> subForm, HorizontalLayout wrapLayout) {
206         HorizontalLayout buttonLayout = new HorizontalLayout();
207         buttonLayout.setStyleName("multi-form-entry-control");
208         buttonLayout.setSpacing(false);
209 
210         if (!(definition.getOrderHandler() instanceof MultiFormDefinition.OrderHandlerDefinition.Noop)) {
211             buttonLayout.addComponents(
212                     new Button(MagnoliaIcons.ARROW2_N, e -> onMove(wrapLayout, true)),
213                     new Button(MagnoliaIcons.ARROW2_S, e -> onMove(wrapLayout, false))
214             );
215         }
216         if (definition.isCanRemoveItems()) {
217             Button removeButton = new Button(MagnoliaIcons.TRASH, e -> onDelete(subForm));
218             removeButton.setDescription(definition.getButtonSelectRemoveLabel());
219             buttonLayout.addComponent(removeButton);
220         }
221 
222         buttonLayout.forEach(button -> button.addStyleName(ResurfaceTheme.BUTTON_ICON));
223         return buttonLayout;
224     }
225 
226     protected void attachAddButton() {
227         Button addButton = new Button(definition.getButtonSelectAddLabel());
228         addButton.addStyleName("add-multi-form-entry-button");
229         addButton.addClickListener(e -> {
230             state.entryResolution.pick().thenAccept(propertyDefinition -> {
231                 if (propertyDefinition != null) {
232                     EditorView<T> subForm = createSubForm(propertyDefinition);
233                     addSubFormToState(subForm, propertyDefinition);
234                     subForm.applyDefaults();
235                     rootLayout.addComponent(wrapChildForm(subForm), rootLayout.getComponentCount() - 1);
236                 }
237             }).exceptionally(ex -> {
238                 log.warn("Failed to create a multi field entry", ex);
239                 Notification.show("Failed to create multi field entry");
240                 return null;
241             });
242         });
243 
244         rootLayout.addComponent(addButton);
245     }
246 
247     private Stream<EditorView<T>> getAllChildren() {
248         return state.children.keySet().stream();
249     }
250 
251     private void onMove(AbstractOrderedLayout movedLayout, boolean moveUp) {
252         int currentPosition = rootLayout.getComponentIndex(movedLayout);
253         int newPosition = moveUp ? currentPosition - 1 : currentPosition + 1;
254 
255         if (currentPosition == 0 && moveUp) {
256             return;
257         }
258 
259         if (newPosition > state.order.size() - 1) {
260             return;
261         }
262 
263         Collections.swap(state.order, currentPosition, newPosition);
264 
265         rootLayout.replaceComponent(markAsDirtyRecursive(currentPosition), markAsDirtyRecursive(newPosition));
266     }
267 
268     private Component markAsDirtyRecursive(int position) {
269         final Component component = rootLayout.getComponent(position);
270         component.markAsDirtyRecursive();
271         return component;
272     }
273 
274     protected void onDelete(EditorView<T> subForm) {
275         rootLayout.removeComponent(subForm.asVaadinComponent().getParent());
276 
277         state.removedItemAccessors.add(state.children.get(subForm));
278         state.children.remove(subForm);
279         state.order.remove(subForm);
280     }
281 
282     protected MultiFormState getState() {
283         return state;
284     }
285 
286     protected Locale getLocale() {
287         return locale;
288     }
289 
290     protected class MultiFormState {
291 
292         Map<EditorView<T>, ItemProviderStrategy<T, T>> children = new LinkedHashMap<>();
293         List<ItemProviderStrategy<T, T>> removedItemAccessors = new ArrayList<>();
294         List<EditorView<T>> order = new ArrayList<>();
295         protected EntryResolution<T> entryResolution;
296 
297         void clear() {
298             children.clear();
299             removedItemAccessors.clear();
300             order.clear();
301         }
302 
303         public List<EditorView<T>> getOrder() {
304             return order;
305         }
306 
307         public EntryResolution<T> getEntryResolution() {
308             return entryResolution;
309         }
310 
311         public Map<EditorView<T>, ItemProviderStrategy<T, T>> getChildren() {
312             return children;
313         }
314     }
315 
316     /**
317      * Multi-form entry resolution strategy.
318      *
319      * @param <T>
320      */
321     public interface EntryResolution<T> {
322 
323         Stream<ComplexPropertyDefinition<T>> resolveForRoot(T rootDatasource);
324 
325         CompletableFuture<ComplexPropertyDefinition<T>> pick();
326 
327         @Getter
328         @Setter
329         @EqualsAndHashCode
330         class Definition<T> implements WithImplementation<EntryResolution<T>> {
331             private Class<? extends EntryResolution<T>> implementationClass;
332         }
333     }
334 }