1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
73
74
75
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
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
318
319
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 }