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 SimpleTranslator i18n;
83 private final Locale locale;
84 private final Datasource<T> datasource;
85 private final MultiFormState state = new MultiFormState();
86
87 private VerticalLayout rootLayout = new VerticalLayout();
88
89 @Inject
90 public MultiFormView(MultiFormDefinition<T> definition, SimpleTranslator i18n, LocaleContext localeContext, Datasource<T> datasource) {
91 this.definition = definition;
92 this.i18n = i18n;
93 this.locale = localeContext.getDefault();
94 this.datasource = datasource;
95 this.state.entryResolution = create(definition.getEntryResolution(), this.locale);
96
97 layout();
98 }
99
100 @Override
101 public List<BinderValidationStatus<?>> validate() {
102 return getAllChildren()
103 .flatMap(view -> view.validate().stream())
104 .collect(toList());
105 }
106
107 @Override
108 public void write(T item) {
109 Map<EditorView<T>, ItemProviderStrategy<T, T>> children = state.children;
110 state.removedItemAccessors.stream()
111 .map(accessor -> accessor.read(item, locale))
112 .forEach(itemToMaybeDelete ->
113 itemToMaybeDelete
114 .ifPresent(this.datasource::remove));
115
116 List<T> itemOrder = new ArrayList<>();
117 state.order.forEach(subForm ->
118 children.get(subForm).read(item, locale).ifPresent(localisedItem -> {
119 itemOrder.add(localisedItem);
120 subForm.write(localisedItem);
121 }));
122
123 create(this.definition.getOrderHandler(), locale).applyOrder(itemOrder);
124 }
125
126 @Override
127 public Component asVaadinComponent() {
128 return rootLayout;
129 }
130
131 @Override
132 public void populate(T item) {
133 getAllChildren().forEach(subForm -> {
134 rootLayout.removeComponent(subForm.asVaadinComponent());
135 subForm.destroy();
136 });
137
138 state.clear();
139 state.entryResolution.resolveForRoot(item).forEach(propertyDefinition -> {
140 if (propertyDefinition != null) {
141 EditorView<T> subForm = createSubForm(propertyDefinition);
142 addSubFormToState(subForm, propertyDefinition);
143 populate(item, subForm);
144 }
145 });
146
147 layout();
148 }
149
150 private EditorView<T> createSubForm(ComplexPropertyDefinition<T> propertyDefinition) {
151 final EditorView<T> subForm = create("child", propertyDefinition.getEditorDefinition());
152 if (subForm.asVaadinComponent().getCaption() == null) {
153 subForm.asVaadinComponent().setCaption(propertyDefinition.getLabel());
154 }
155 return subForm;
156 }
157
158 private void addSubFormToState(EditorView<T> subForm, ComplexPropertyDefinition<T> propertyDefinition) {
159 ItemProviderStrategy<T, T> itemProviderStrategy = create(propertyDefinition.getItemProvider());
160 state.children.put(subForm, itemProviderStrategy);
161 state.order.add(subForm);
162 }
163
164 private void populate(T item, EditorView<T> subForm) {
165 state.children.get(subForm).read(item, locale).ifPresent(subForm::populate);
166 }
167
168 public void layout() {
169 rootLayout.removeAllComponents();
170 rootLayout.setMargin(false);
171 state.children.keySet().stream()
172 .map(this::wrapChildForm)
173 .forEach(this.rootLayout::addComponent);
174 attachAddButton();
175 }
176
177 protected Component wrapChildForm(EditorView<T> subForm) {
178 HorizontalLayout wrapLayout = new HorizontalLayout();
179 wrapLayout.setWidth("100%");
180
181 relaySubFormCaptionToWrapper(subForm, wrapLayout);
182
183 HorizontalLayout buttonLayout = createButtonsLayout(subForm, wrapLayout);
184 wrapLayout.addComponents(subForm.asVaadinComponent(), buttonLayout);
185 wrapLayout.setExpandRatio(subForm.asVaadinComponent(), 1f);
186 wrapLayout.setComponentAlignment(buttonLayout, Alignment.MIDDLE_CENTER);
187
188 return wrapLayout;
189 }
190
191 private void relaySubFormCaptionToWrapper(EditorView<T> subForm, HorizontalLayout wrapLayout) {
192 wrapLayout.setCaption(subForm.asVaadinComponent().getCaption());
193 subForm.asVaadinComponent().setCaption(null);
194 }
195
196 private HorizontalLayout createButtonsLayout(EditorView<T> subForm, HorizontalLayout wrapLayout) {
197 HorizontalLayout buttonLayout = new HorizontalLayout();
198 buttonLayout.setSpacing(false);
199
200 if (!(definition.getOrderHandler() instanceof MultiFormDefinition.OrderHandlerDefinition.Noop)) {
201 buttonLayout.addComponents(
202 new Button(MagnoliaIcons.ARROW2_N, e -> onMove(wrapLayout, true)),
203 new Button(MagnoliaIcons.ARROW2_S, e -> onMove(wrapLayout, false))
204 );
205 }
206 if (definition.isCanRemoveItems()) {
207 buttonLayout.addComponent(new Button(MagnoliaIcons.TRASH, e -> onDelete(subForm)));
208 }
209
210 buttonLayout.forEach(button -> button.addStyleName(ResurfaceTheme.BUTTON_ICON));
211 return buttonLayout;
212 }
213
214 protected void attachAddButton() {
215 Button addButton = new Button(this.i18n.translate("buttons.add"));
216 addButton.addClickListener(e -> {
217 state.entryResolution.pick().thenAccept(propertyDefinition -> {
218 if (propertyDefinition != null) {
219 EditorView<T> subForm = createSubForm(propertyDefinition);
220 addSubFormToState(subForm, propertyDefinition);
221 rootLayout.addComponent(wrapChildForm(subForm), rootLayout.getComponentCount() - 1);
222 }
223 }).exceptionally(ex -> {
224 log.warn("Failed to create a multi field entry", ex);
225 Notification.show("Failed to create multi field entry");
226 return null;
227 });
228 });
229
230 rootLayout.addComponent(addButton);
231 }
232
233 private Stream<EditorView<T>> getAllChildren() {
234 return state.children.keySet().stream();
235 }
236
237 private void onMove(AbstractOrderedLayout movedLayout, boolean moveUp) {
238 int currentPosition = rootLayout.getComponentIndex(movedLayout);
239 int newPosition = moveUp ? currentPosition - 1 : currentPosition + 1;
240
241 if (currentPosition == 0 && moveUp) {
242 return;
243 }
244
245 if (newPosition >= rootLayout.getComponentCount() - 1) {
246 return;
247 }
248
249 Collections.swap(state.order, currentPosition, newPosition);
250 rootLayout.replaceComponent(rootLayout.getComponent(currentPosition), rootLayout.getComponent(newPosition));
251 }
252
253 private void onDelete(EditorView<T> subForm) {
254 rootLayout.removeComponent(subForm.asVaadinComponent().getParent());
255
256 state.removedItemAccessors.add(state.children.get(subForm));
257 state.children.remove(subForm);
258 state.order.remove(subForm);
259 }
260
261 class MultiFormState {
262 Map<EditorView<T>, ItemProviderStrategy<T, T>> children = new LinkedHashMap<>();
263 List<ItemProviderStrategy<T, T>> removedItemAccessors = new ArrayList<>();
264 List<EditorView<T>> order = new ArrayList<>();
265 EntryResolution<T> entryResolution;
266
267 void clear() {
268 children.clear();
269 removedItemAccessors.clear();
270 order.clear();
271 }
272 }
273
274
275
276
277
278
279 public interface EntryResolution<T> {
280
281 Stream<ComplexPropertyDefinition<T>> resolveForRoot(T rootDatasource);
282
283 CompletableFuture<ComplexPropertyDefinition<T>> pick();
284
285 @Getter
286 @Setter
287 @EqualsAndHashCode
288 class Definition<T> implements WithImplementation<EntryResolution<T>> {
289 private Class<? extends EntryResolution<T>> implementationClass;
290 }
291 }
292 }