SEBSERV-28 # first implementation with sort and navigation
still left : filtering
This commit is contained in:
		
							parent
							
								
									94a75735ee
								
							
						
					
					
						commit
						8f10dd6163
					
				
					 28 changed files with 876 additions and 59 deletions
				
			
		|  | @ -16,6 +16,7 @@ public final class Constants { | ||||||
| 
 | 
 | ||||||
|     public static final Character LIST_SEPARATOR_CHAR = ','; |     public static final Character LIST_SEPARATOR_CHAR = ','; | ||||||
|     public static final String LIST_SEPARATOR = ","; |     public static final String LIST_SEPARATOR = ","; | ||||||
|  |     public static final String EMPTY_NOTE = "--"; | ||||||
| 
 | 
 | ||||||
|     /** Date-Time formatter without milliseconds using UTC time-zone. Pattern is yyyy-MM-dd HH:mm:ss */ |     /** Date-Time formatter without milliseconds using UTC time-zone. Pattern is yyyy-MM-dd HH:mm:ss */ | ||||||
|     public static final DateTimeFormatter DATE_TIME_PATTERN_UTC_NO_MILLIS = DateTimeFormat |     public static final DateTimeFormatter DATE_TIME_PATTERN_UTC_NO_MILLIS = DateTimeFormat | ||||||
|  |  | ||||||
|  | @ -22,4 +22,6 @@ public class SEBServerRestEndpoints { | ||||||
| 
 | 
 | ||||||
|     public static final String ENDPOINT_USER_ACTIVITY_LOG = "/useractivity"; |     public static final String ENDPOINT_USER_ACTIVITY_LOG = "/useractivity"; | ||||||
| 
 | 
 | ||||||
|  |     public static final String NAMES_ENDPOINT_SUFFIX = "/names"; | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -51,16 +51,16 @@ public final class Page<T> { | ||||||
|         this.sort = sort; |         this.sort = sort; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Integer getNumberOfPages() { |     public int getNumberOfPages() { | ||||||
|         return this.numberOfPages; |         return (this.numberOfPages != null) ? this.numberOfPages : 1; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Integer getPageNumber() { |     public int getPageNumber() { | ||||||
|         return this.pageNumber; |         return (this.pageNumber != null) ? this.pageNumber : 0; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Integer getPageSize() { |     public int getPageSize() { | ||||||
|         return this.pageSize; |         return (this.pageSize != null) ? this.pageSize : -1; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Collection<T> getContent() { |     public Collection<T> getContent() { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,28 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.gui.service; | ||||||
|  | 
 | ||||||
|  | import org.eclipse.swt.widgets.Composite; | ||||||
|  | import org.eclipse.swt.widgets.Control; | ||||||
|  | 
 | ||||||
|  | public final class RWTUtils { | ||||||
|  | 
 | ||||||
|  |     public static final String TEXT_NAME_H2 = "h2"; | ||||||
|  | 
 | ||||||
|  |     public static void clearComposite(final Composite parent) { | ||||||
|  |         if (parent == null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (final Control control : parent.getChildren()) { | ||||||
|  |             control.dispose(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -16,6 +16,7 @@ public interface PolyglotPageService { | ||||||
| 
 | 
 | ||||||
|     String POLYGLOT_WIDGET_FUNCTION_KEY = "POLYGLOT_WIDGET_FUNCTION"; |     String POLYGLOT_WIDGET_FUNCTION_KEY = "POLYGLOT_WIDGET_FUNCTION"; | ||||||
|     String POLYGLOT_TREE_ITEM_TEXT_DATA_KEY = "POLYGLOT_TREE_ITEM_TEXT_DATA"; |     String POLYGLOT_TREE_ITEM_TEXT_DATA_KEY = "POLYGLOT_TREE_ITEM_TEXT_DATA"; | ||||||
|  |     String POLYGLOT_TREE_ITEM_TOOLTIP_DATA_KEY = "POLYGLOT_TREE_ITEM_TOOLTIP_DATA"; | ||||||
| 
 | 
 | ||||||
|     /** Gets the underling I18nSupport |     /** Gets the underling I18nSupport | ||||||
|      * |      * | ||||||
|  |  | ||||||
|  | @ -10,10 +10,10 @@ package ch.ethz.seb.sebserver.gui.service.page; | ||||||
| 
 | 
 | ||||||
| public interface TemplateComposer { | public interface TemplateComposer { | ||||||
| 
 | 
 | ||||||
|     default boolean validate(final PageContext context) { |     default boolean validate(final PageContext pageContext) { | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     void compose(PageContext composerCtx); |     void compose(PageContext pageContext); | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; | ||||||
| import ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys; | import ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys; | ||||||
| import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; | import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; | ||||||
| import ch.ethz.seb.sebserver.gui.service.page.action.ActionPane; | import ch.ethz.seb.sebserver.gui.service.page.action.ActionPane; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.page.content.InstitutionList; | ||||||
| import ch.ethz.seb.sebserver.gui.service.page.impl.TODOTemplate; | import ch.ethz.seb.sebserver.gui.service.page.impl.TODOTemplate; | ||||||
| 
 | 
 | ||||||
| public class ActivitySelection { | public class ActivitySelection { | ||||||
|  | @ -31,7 +32,7 @@ public class ActivitySelection { | ||||||
|     public enum Activity { |     public enum Activity { | ||||||
|         NONE(TODOTemplate.class, TODOTemplate.class, (String) null), |         NONE(TODOTemplate.class, TODOTemplate.class, (String) null), | ||||||
|         INSTITUTION_ROOT( |         INSTITUTION_ROOT( | ||||||
|                 TODOTemplate.class, |                 InstitutionList.class, | ||||||
|                 ActionPane.class, |                 ActionPane.class, | ||||||
|                 new LocTextKey("sebserver.activities.inst")), |                 new LocTextKey("sebserver.activities.inst")), | ||||||
|         INSTITUTION_NODE( |         INSTITUTION_NODE( | ||||||
|  |  | ||||||
|  | @ -0,0 +1,86 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.gui.service.page.content; | ||||||
|  | 
 | ||||||
|  | import org.eclipse.swt.SWT; | ||||||
|  | import org.eclipse.swt.layout.GridData; | ||||||
|  | import org.eclipse.swt.layout.GridLayout; | ||||||
|  | import org.eclipse.swt.widgets.Composite; | ||||||
|  | import org.springframework.context.annotation.Lazy; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.Domain; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.page.PageContext; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitutions; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.table.ColumnDefinition; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.widget.WidgetFactory; | ||||||
|  | 
 | ||||||
|  | @Lazy | ||||||
|  | @Component | ||||||
|  | @GuiProfile | ||||||
|  | public class InstitutionList implements TemplateComposer { | ||||||
|  | 
 | ||||||
|  |     private final WidgetFactory widgetFactory; | ||||||
|  |     private final RestService restService; | ||||||
|  | 
 | ||||||
|  |     protected InstitutionList( | ||||||
|  |             final WidgetFactory widgetFactory, | ||||||
|  |             final RestService restService) { | ||||||
|  | 
 | ||||||
|  |         this.widgetFactory = widgetFactory; | ||||||
|  |         this.restService = restService; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void compose(final PageContext pageContext) { | ||||||
|  |         final Composite content = new Composite(pageContext.getParent(), SWT.NONE); | ||||||
|  |         final GridLayout contentLayout = new GridLayout(); | ||||||
|  |         contentLayout.marginLeft = 10; | ||||||
|  |         content.setLayout(contentLayout); | ||||||
|  |         content.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); | ||||||
|  | 
 | ||||||
|  |         this.widgetFactory.labelLocalizedTitle( | ||||||
|  |                 content, | ||||||
|  |                 new LocTextKey("sebserver.institution.list.title")); | ||||||
|  | 
 | ||||||
|  |         this.widgetFactory.entityTableBuilder(this.restService.getRestCall(GetInstitutions.class)) | ||||||
|  |                 .withPaging(3) | ||||||
|  |                 .withColumn(new ColumnDefinition<>( | ||||||
|  |                         Domain.INSTITUTION.ATTR_NAME, | ||||||
|  |                         new LocTextKey("sebserver.institution.list.column.name"), | ||||||
|  |                         null, | ||||||
|  |                         0, | ||||||
|  |                         entity -> entity.name, | ||||||
|  |                         null, | ||||||
|  |                         true)) | ||||||
|  |                 .withColumn(new ColumnDefinition<>( | ||||||
|  |                         Domain.INSTITUTION.ATTR_URL_SUFFIX, | ||||||
|  |                         new LocTextKey("sebserver.institution.list.column.urlSuffix"), | ||||||
|  |                         null, | ||||||
|  |                         0, | ||||||
|  |                         entity -> entity.urlSuffix, | ||||||
|  |                         null, | ||||||
|  |                         true)) | ||||||
|  |                 .withColumn(new ColumnDefinition<>( | ||||||
|  |                         Domain.INSTITUTION.ATTR_ACTIVE, | ||||||
|  |                         new LocTextKey("sebserver.institution.list.column.active"), | ||||||
|  |                         null, | ||||||
|  |                         0, | ||||||
|  |                         entity -> entity.active, | ||||||
|  |                         null, | ||||||
|  |                         true)) | ||||||
|  |                 .compose(content); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -14,13 +14,13 @@ import java.util.function.Function; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
| 
 | 
 | ||||||
| import org.eclipse.swt.widgets.Composite; | import org.eclipse.swt.widgets.Composite; | ||||||
| import org.eclipse.swt.widgets.Control; |  | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| import org.springframework.context.annotation.Lazy; | import org.springframework.context.annotation.Lazy; | ||||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||||
| 
 | 
 | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; | import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.RWTUtils; | ||||||
| import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; | import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; | ||||||
| import ch.ethz.seb.sebserver.gui.service.page.ComposerService; | import ch.ethz.seb.sebserver.gui.service.page.ComposerService; | ||||||
| import ch.ethz.seb.sebserver.gui.service.page.PageContext; | import ch.ethz.seb.sebserver.gui.service.page.PageContext; | ||||||
|  | @ -108,7 +108,7 @@ public class ComposerServiceImpl implements ComposerService { | ||||||
| 
 | 
 | ||||||
|         if (composer.validate(pageContext)) { |         if (composer.validate(pageContext)) { | ||||||
| 
 | 
 | ||||||
|             clear(pageContext.getParent()); |             RWTUtils.clearComposite(pageContext.getParent()); | ||||||
| 
 | 
 | ||||||
|             try { |             try { | ||||||
|                 composer.compose(pageContext); |                 composer.compose(pageContext); | ||||||
|  | @ -173,14 +173,4 @@ public class ComposerServiceImpl implements ComposerService { | ||||||
|                 this.i18nSupport, this, root, root, null); |                 this.i18nSupport, this, root, root, null); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void clear(final Composite parent) { |  | ||||||
|         if (parent == null) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (final Control control : parent.getChildren()) { |  | ||||||
|             control.dispose(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,17 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.gui.service.remote.webservice.api; | ||||||
|  | 
 | ||||||
|  | import org.springframework.util.MultiValueMap; | ||||||
|  | 
 | ||||||
|  | public interface FilterAttributeSupplier { | ||||||
|  | 
 | ||||||
|  |     MultiValueMap<String, String> getAttributes(); | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -30,6 +30,7 @@ import com.fasterxml.jackson.core.type.TypeReference; | ||||||
| 
 | 
 | ||||||
| import ch.ethz.seb.sebserver.gbl.api.APIMessage; | import ch.ethz.seb.sebserver.gbl.api.APIMessage; | ||||||
| import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.Entity; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.Page; | import ch.ethz.seb.sebserver.gbl.model.Page; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService.SortOrder; | import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService.SortOrder; | ||||||
|  | @ -166,7 +167,21 @@ public abstract class RestCall<T> { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public RestCallBuilder withSorting(final String column, final SortOrder order) { |         public RestCallBuilder withSorting(final String column, final SortOrder order) { | ||||||
|             this.queryParams.put(Page.ATTR_SORT, Arrays.asList(order.prefix + column)); |             if (column != null) { | ||||||
|  |                 this.queryParams.put(Page.ATTR_SORT, Arrays.asList(order.encode(column))); | ||||||
|  |             } | ||||||
|  |             return this; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public RestCallBuilder withFilterAttributes(final FilterAttributeSupplier filterAttributes) { | ||||||
|  |             if (filterAttributes != null) { | ||||||
|  |                 this.queryParams.putAll(filterAttributes.getAttributes()); | ||||||
|  |             } | ||||||
|  |             return this; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public RestCallBuilder onlyActive(final boolean active) { | ||||||
|  |             this.queryParams.put(Entity.FILTER_ATTR_ACTIVE, Arrays.asList(String.valueOf(active))); | ||||||
|             return this; |             return this; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -188,6 +203,7 @@ public abstract class RestCall<T> { | ||||||
|                 return new HttpEntity<>(this.httpHeaders); |                 return new HttpEntity<>(this.httpHeaders); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -33,7 +33,13 @@ public class GetInstitutionNames extends RestCall<List<EntityName>> { | ||||||
|                 }, |                 }, | ||||||
|                 HttpMethod.GET, |                 HttpMethod.GET, | ||||||
|                 MediaType.APPLICATION_FORM_URLENCODED, |                 MediaType.APPLICATION_FORM_URLENCODED, | ||||||
|                 SEBServerRestEndpoints.ENDPOINT_INSTITUTION + "/names"); |                 SEBServerRestEndpoints.ENDPOINT_INSTITUTION + SEBServerRestEndpoints.NAMES_ENDPOINT_SUFFIX); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public RestCall<List<EntityName>>.RestCallBuilder newBuilder() { | ||||||
|  |         return super.newBuilder() | ||||||
|  |                 .onlyActive(true); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,38 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution; | ||||||
|  | 
 | ||||||
|  | import org.springframework.context.annotation.Lazy; | ||||||
|  | import org.springframework.http.HttpMethod; | ||||||
|  | import org.springframework.http.MediaType; | ||||||
|  | import org.springframework.stereotype.Component; | ||||||
|  | 
 | ||||||
|  | import com.fasterxml.jackson.core.type.TypeReference; | ||||||
|  | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.api.SEBServerRestEndpoints; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.Page; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.institution.Institution; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; | ||||||
|  | 
 | ||||||
|  | @Lazy | ||||||
|  | @Component | ||||||
|  | @GuiProfile | ||||||
|  | public class GetInstitutions extends RestCall<Page<Institution>> { | ||||||
|  | 
 | ||||||
|  |     protected GetInstitutions() { | ||||||
|  |         super( | ||||||
|  |                 new TypeReference<Page<Institution>>() { | ||||||
|  |                 }, | ||||||
|  |                 HttpMethod.GET, | ||||||
|  |                 MediaType.APPLICATION_FORM_URLENCODED, | ||||||
|  |                 SEBServerRestEndpoints.ENDPOINT_INSTITUTION); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,60 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.gui.service.table; | ||||||
|  | 
 | ||||||
|  | import java.util.function.Function; | ||||||
|  | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.Entity; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; | ||||||
|  | 
 | ||||||
|  | public final class ColumnDefinition<ROW extends Entity> { | ||||||
|  | 
 | ||||||
|  |     final String columnName; | ||||||
|  |     final LocTextKey displayName; | ||||||
|  |     final LocTextKey tooltip; | ||||||
|  |     final int widthPercent; | ||||||
|  |     final Function<ROW, Object> valueSupplier; | ||||||
|  |     final ColumnFilterDefinition filter; | ||||||
|  |     final boolean sortable; | ||||||
|  | 
 | ||||||
|  |     public ColumnDefinition( | ||||||
|  |             final String columnName, | ||||||
|  |             final LocTextKey displayName, | ||||||
|  |             final LocTextKey tooltip, | ||||||
|  |             final int widthPercent) { | ||||||
|  | 
 | ||||||
|  |         this(columnName, displayName, tooltip, widthPercent, null, null, false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public ColumnDefinition( | ||||||
|  |             final String columnName, | ||||||
|  |             final LocTextKey displayName, | ||||||
|  |             final int widthPercent) { | ||||||
|  | 
 | ||||||
|  |         this(columnName, displayName, null, widthPercent, null, null, false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public ColumnDefinition( | ||||||
|  |             final String columnName, | ||||||
|  |             final LocTextKey displayName, | ||||||
|  |             final LocTextKey tooltip, | ||||||
|  |             final int widthPercent, | ||||||
|  |             final Function<ROW, Object> valueSupplier, | ||||||
|  |             final ColumnFilterDefinition filter, | ||||||
|  |             final boolean sortable) { | ||||||
|  | 
 | ||||||
|  |         this.columnName = columnName; | ||||||
|  |         this.displayName = displayName; | ||||||
|  |         this.tooltip = tooltip; | ||||||
|  |         this.widthPercent = widthPercent; | ||||||
|  |         this.valueSupplier = valueSupplier; | ||||||
|  |         this.filter = filter; | ||||||
|  |         this.sortable = sortable; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,13 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  *  | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.gui.service.table; | ||||||
|  | 
 | ||||||
|  | public class ColumnFilterDefinition { | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,250 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.gui.service.table; | ||||||
|  | 
 | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import org.eclipse.swt.SWT; | ||||||
|  | import org.eclipse.swt.layout.GridData; | ||||||
|  | import org.eclipse.swt.layout.GridLayout; | ||||||
|  | import org.eclipse.swt.widgets.Composite; | ||||||
|  | import org.eclipse.swt.widgets.Event; | ||||||
|  | import org.eclipse.swt.widgets.Table; | ||||||
|  | import org.eclipse.swt.widgets.TableColumn; | ||||||
|  | import org.eclipse.swt.widgets.TableItem; | ||||||
|  | import org.slf4j.Logger; | ||||||
|  | import org.slf4j.LoggerFactory; | ||||||
|  | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.Entity; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.Page; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.widget.WidgetFactory; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService.SortOrder; | ||||||
|  | 
 | ||||||
|  | public class EntityTable<ROW extends Entity> extends Composite { | ||||||
|  | 
 | ||||||
|  |     private static final Logger log = LoggerFactory.getLogger(EntityTable.class); | ||||||
|  | 
 | ||||||
|  |     private static final long serialVersionUID = -4931198225547108993L; | ||||||
|  | 
 | ||||||
|  |     public static final String TABLE_ROW_DATA = "TABLE_ROW_DATA"; | ||||||
|  | 
 | ||||||
|  |     private final WidgetFactory widgetFactory; | ||||||
|  | 
 | ||||||
|  |     private final RestCall<Page<ROW>> restCall; | ||||||
|  |     private final List<ColumnDefinition<ROW>> columns; | ||||||
|  |     private final List<TableRowAction> actions; | ||||||
|  | 
 | ||||||
|  |     private final TableFilter<ROW> filter; | ||||||
|  |     private final Table table; | ||||||
|  |     private final TableNavigator navigator; | ||||||
|  | 
 | ||||||
|  |     private final boolean selectableRows; | ||||||
|  | 
 | ||||||
|  |     private int pageNumber = 1; | ||||||
|  |     private int pageSize; | ||||||
|  |     private String sortColumn = null; | ||||||
|  |     private SortOrder sortOrder = SortOrder.ASCENDING; | ||||||
|  | 
 | ||||||
|  |     private boolean columnsWithSameWidth = true; | ||||||
|  | 
 | ||||||
|  |     EntityTable( | ||||||
|  |             final Composite parent, | ||||||
|  |             final RestCall<Page<ROW>> restCall, | ||||||
|  |             final WidgetFactory widgetFactory, | ||||||
|  |             final List<ColumnDefinition<ROW>> columns, | ||||||
|  |             final List<TableRowAction> actions, | ||||||
|  |             final int pageSize, | ||||||
|  |             final boolean withFilter, | ||||||
|  |             final boolean selectableRows) { | ||||||
|  | 
 | ||||||
|  |         super(parent, SWT.NONE); | ||||||
|  |         this.widgetFactory = widgetFactory; | ||||||
|  |         this.restCall = restCall; | ||||||
|  |         this.columns = Utils.immutableListOf(columns); | ||||||
|  |         this.actions = Utils.immutableListOf(actions); | ||||||
|  | 
 | ||||||
|  |         super.setLayout(new GridLayout()); | ||||||
|  |         super.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); | ||||||
|  | 
 | ||||||
|  |         this.pageSize = pageSize; | ||||||
|  |         this.filter = (withFilter) ? new TableFilter<>(this) : null; | ||||||
|  |         this.selectableRows = selectableRows; | ||||||
|  | 
 | ||||||
|  |         this.table = widgetFactory.tableLocalized(this); | ||||||
|  |         this.table.setLayout(new GridLayout(columns.size(), true)); | ||||||
|  |         final GridData gridData = new GridData(SWT.FILL, SWT.CENTER, true, false); | ||||||
|  |         this.table.setLayoutData(gridData); | ||||||
|  |         this.table.addListener(SWT.Resize, this::adaptColumnWidth); | ||||||
|  | 
 | ||||||
|  |         //this.table.setLayoutData(new GridData(GridData.FILL_BOTH)); | ||||||
|  |         this.table.setHeaderVisible(true); | ||||||
|  |         this.table.setLinesVisible(true); | ||||||
|  | 
 | ||||||
|  |         this.navigator = new TableNavigator(this); | ||||||
|  | 
 | ||||||
|  |         createTableColumns(); | ||||||
|  |         updateTableRows( | ||||||
|  |                 this.pageNumber, | ||||||
|  |                 this.pageSize, | ||||||
|  |                 this.sortColumn, | ||||||
|  |                 this.sortOrder); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setPageSize(final int pageSize) { | ||||||
|  |         this.pageSize = pageSize; | ||||||
|  |         updateTableRows( | ||||||
|  |                 this.pageNumber, | ||||||
|  |                 this.pageSize, | ||||||
|  |                 this.sortColumn, | ||||||
|  |                 this.sortOrder); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void selectPage(final int pageSelection) { | ||||||
|  |         // verify input | ||||||
|  |         this.pageNumber = pageSelection; | ||||||
|  |         if (this.pageNumber < 1) { | ||||||
|  |             this.pageNumber = 1; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         updateTableRows( | ||||||
|  |                 this.pageNumber, | ||||||
|  |                 this.pageSize, | ||||||
|  |                 this.sortColumn, | ||||||
|  |                 this.sortOrder); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void applyFilter() { | ||||||
|  |         // TODO remove all rows, set current page to 0, call rest to get entities and build rows and navigation again | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void applySort(final String columnName) { | ||||||
|  |         this.sortColumn = columnName; | ||||||
|  |         this.sortOrder = SortOrder.ASCENDING; | ||||||
|  | 
 | ||||||
|  |         updateTableRows( | ||||||
|  |                 this.pageNumber, | ||||||
|  |                 this.pageSize, | ||||||
|  |                 this.sortColumn, | ||||||
|  |                 this.sortOrder); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void changeSortOrder() { | ||||||
|  |         this.sortOrder = (this.sortOrder == SortOrder.ASCENDING) | ||||||
|  |                 ? SortOrder.DESCENDING | ||||||
|  |                 : SortOrder.ASCENDING; | ||||||
|  | 
 | ||||||
|  |         updateTableRows( | ||||||
|  |                 this.pageNumber, | ||||||
|  |                 this.pageSize, | ||||||
|  |                 this.sortColumn, | ||||||
|  |                 this.sortOrder); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void createTableColumns() { | ||||||
|  |         for (final ColumnDefinition<ROW> column : this.columns) { | ||||||
|  |             final TableColumn tableColumn = this.widgetFactory.tableColumnLocalized( | ||||||
|  |                     this.table, | ||||||
|  |                     column.displayName, | ||||||
|  |                     column.tooltip); | ||||||
|  | 
 | ||||||
|  |             if (column.sortable) { | ||||||
|  |                 tableColumn.addListener(SWT.Selection, event -> { | ||||||
|  |                     if (!column.columnName.equals(this.sortColumn)) { | ||||||
|  |                         applySort(column.columnName); | ||||||
|  |                         this.table.setSortColumn(tableColumn); | ||||||
|  |                         this.table.setSortDirection(SWT.UP); | ||||||
|  |                     } else { | ||||||
|  |                         changeSortOrder(); | ||||||
|  |                         this.table.setSortDirection( | ||||||
|  |                                 (this.sortOrder == SortOrder.ASCENDING) ? SWT.UP : SWT.DOWN); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (column.widthPercent > 0) { | ||||||
|  |                 this.columnsWithSameWidth = false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void updateTableRows( | ||||||
|  |             final int pageNumber, | ||||||
|  |             final int pageSize, | ||||||
|  |             final String sortColumn, | ||||||
|  |             final SortOrder sortOrder) { | ||||||
|  | 
 | ||||||
|  |         // first remove all rows if there are some | ||||||
|  |         this.table.removeAll(); | ||||||
|  | 
 | ||||||
|  |         // get page data and create rows | ||||||
|  |         this.restCall.newBuilder() | ||||||
|  |                 .withPaging(pageNumber, pageSize) | ||||||
|  |                 .withSorting(sortColumn, sortOrder) | ||||||
|  |                 .withFilterAttributes(this.filter) | ||||||
|  |                 .call() | ||||||
|  |                 .map(this::createTableRowsFromPage) | ||||||
|  |                 .map(this.navigator::update) | ||||||
|  |                 .onErrorDo(t -> { | ||||||
|  |                     // TODO error handling | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |         this.layout(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private Page<ROW> createTableRowsFromPage(final Page<ROW> page) { | ||||||
|  |         for (final ROW row : page.content) { | ||||||
|  |             final TableItem item = new TableItem(this.table, SWT.NONE); | ||||||
|  |             item.setData(TABLE_ROW_DATA, row); | ||||||
|  |             int index = 0; | ||||||
|  |             if (this.selectableRows) { | ||||||
|  |                 // TODO | ||||||
|  |             } | ||||||
|  |             for (final ColumnDefinition<ROW> column : this.columns) { | ||||||
|  |                 final Object value = column.valueSupplier.apply(row); | ||||||
|  |                 if (value instanceof Boolean) { | ||||||
|  |                     // TODO set an image or HTML with checkbox | ||||||
|  |                     item.setText(index, String.valueOf(value)); | ||||||
|  |                 } else { | ||||||
|  |                     item.setText(index, String.valueOf(value)); | ||||||
|  |                 } | ||||||
|  |                 index++; | ||||||
|  |             } | ||||||
|  |             if (this.actions != null) { | ||||||
|  |                 // TODO | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return page; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void adaptColumnWidth(final Event event) { | ||||||
|  |         try { | ||||||
|  |             final int currentTableWidth = this.table.getParent().getClientArea().width; | ||||||
|  | 
 | ||||||
|  |             int index = 0; | ||||||
|  |             for (final ColumnDefinition<ROW> column : this.columns) { | ||||||
|  | 
 | ||||||
|  |                 final int percentage = (this.columnsWithSameWidth) | ||||||
|  |                         ? 100 / this.columns.size() | ||||||
|  |                         : column.widthPercent; | ||||||
|  | 
 | ||||||
|  |                 final TableColumn tableColumn = this.table.getColumn(index); | ||||||
|  |                 tableColumn.setWidth(currentTableWidth / 100 * percentage); | ||||||
|  | 
 | ||||||
|  |                 index++; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             log.warn("Failed to adaptColumnWidth: ", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,92 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.gui.service.table; | ||||||
|  | 
 | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import org.eclipse.swt.widgets.Composite; | ||||||
|  | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.Entity; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.Page; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.widget.WidgetFactory; | ||||||
|  | 
 | ||||||
|  | /** <code> | ||||||
|  |  *  new TableBuilder<T>(RestCall) | ||||||
|  |  *      .withPaging(pageSize) | ||||||
|  |  *      .withColumn(new ColumnDefinition( | ||||||
|  |  *          columnName:String, | ||||||
|  |  *          displayName:LocTextKey, | ||||||
|  |  *          tooltip:LocTextKey, | ||||||
|  |  *          width:int, | ||||||
|  |  *          valueSupplier:Function<ROW, String>, | ||||||
|  |  *          sortable:boolean, | ||||||
|  |  *          columnFilter:TableColumnFilter)) | ||||||
|  |  *      .withAction(action:TableRowAction) | ||||||
|  |  *      .withSelectableRows(boolean) | ||||||
|  |  *      .compose(parent:Composit, group:Composite); | ||||||
|  |  * </code> */ | ||||||
|  | public class TableBuilder<ROW extends Entity> { | ||||||
|  | 
 | ||||||
|  |     private final WidgetFactory widgetFactory; | ||||||
|  |     final RestCall<Page<ROW>> restCall; | ||||||
|  |     final List<ColumnDefinition<ROW>> columns = new ArrayList<>(); | ||||||
|  |     final List<TableRowAction> actions = new ArrayList<>(); | ||||||
|  | 
 | ||||||
|  |     private int pageSize = -1; | ||||||
|  |     private boolean selectableRows = false; | ||||||
|  | 
 | ||||||
|  |     public TableBuilder( | ||||||
|  |             final WidgetFactory widgetFactory, | ||||||
|  |             final RestCall<Page<ROW>> restCall) { | ||||||
|  | 
 | ||||||
|  |         this.widgetFactory = widgetFactory; | ||||||
|  |         this.restCall = restCall; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public TableBuilder<ROW> withPaging(final int pageSize) { | ||||||
|  |         this.pageSize = pageSize; | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public TableBuilder<ROW> withColumn(final ColumnDefinition<ROW> columnDef) { | ||||||
|  |         this.columns.add(columnDef); | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public TableBuilder<ROW> withSelectableRows() { | ||||||
|  |         this.selectableRows = true; | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public TableBuilder<ROW> withAction(final TableRowAction action) { | ||||||
|  |         this.actions.add(action); | ||||||
|  |         return this; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public EntityTable<ROW> compose(final Composite parent) { | ||||||
|  |         final boolean withFilter = this.columns | ||||||
|  |                 .stream() | ||||||
|  |                 .filter(c -> c.filter != null) | ||||||
|  |                 .findFirst() | ||||||
|  |                 .isPresent(); | ||||||
|  | 
 | ||||||
|  |         return new EntityTable<>( | ||||||
|  |                 parent, | ||||||
|  |                 this.restCall, | ||||||
|  |                 this.widgetFactory, | ||||||
|  |                 this.columns, | ||||||
|  |                 this.actions, | ||||||
|  |                 this.pageSize, | ||||||
|  |                 withFilter, | ||||||
|  |                 this.selectableRows); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,31 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.gui.service.table; | ||||||
|  | 
 | ||||||
|  | import org.eclipse.swt.SWT; | ||||||
|  | import org.eclipse.swt.widgets.Composite; | ||||||
|  | import org.springframework.util.MultiValueMap; | ||||||
|  | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.Entity; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.FilterAttributeSupplier; | ||||||
|  | 
 | ||||||
|  | public class TableFilter<ROW extends Entity> extends Composite implements FilterAttributeSupplier { | ||||||
|  | 
 | ||||||
|  |     private static final long serialVersionUID = -2460403977147440766L; | ||||||
|  | 
 | ||||||
|  |     TableFilter(final EntityTable<ROW> parent) { | ||||||
|  |         super(parent, SWT.NONE); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public MultiValueMap<String, String> getAttributes() { | ||||||
|  |         // TODO Auto-generated method stub | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,112 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  * | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.gui.service.table; | ||||||
|  | 
 | ||||||
|  | import org.eclipse.rap.rwt.RWT; | ||||||
|  | import org.eclipse.swt.SWT; | ||||||
|  | import org.eclipse.swt.layout.GridData; | ||||||
|  | import org.eclipse.swt.layout.GridLayout; | ||||||
|  | import org.eclipse.swt.layout.RowLayout; | ||||||
|  | import org.eclipse.swt.widgets.Composite; | ||||||
|  | import org.eclipse.swt.widgets.Label; | ||||||
|  | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.Page; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.RWTUtils; | ||||||
|  | 
 | ||||||
|  | public class TableNavigator extends Composite { | ||||||
|  | 
 | ||||||
|  |     private static final long serialVersionUID = -7349918232061226192L; | ||||||
|  | 
 | ||||||
|  |     private final int pageNavSize = 3; | ||||||
|  |     private final EntityTable<?> entityTable; | ||||||
|  | 
 | ||||||
|  |     TableNavigator(final EntityTable<?> entityTable) { | ||||||
|  |         super(entityTable, SWT.NONE); | ||||||
|  |         super.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); | ||||||
|  | 
 | ||||||
|  |         final GridLayout layout = new GridLayout(3, true); | ||||||
|  |         layout.marginLeft = 20; | ||||||
|  |         super.setLayout(layout); | ||||||
|  | 
 | ||||||
|  |         this.entityTable = entityTable; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Page<?> update(final Page<?> pageData) { | ||||||
|  |         // clear all | ||||||
|  |         RWTUtils.clearComposite(this); | ||||||
|  | 
 | ||||||
|  |         final int pageNumber = pageData.getPageNumber(); | ||||||
|  |         final int numberOfPages = pageData.getNumberOfPages(); | ||||||
|  | 
 | ||||||
|  |         createPagingHeader(pageNumber, numberOfPages); | ||||||
|  | 
 | ||||||
|  |         final Composite numNav = new Composite(this, SWT.NONE); | ||||||
|  |         numNav.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); | ||||||
|  |         final RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL); | ||||||
|  |         rowLayout.spacing = 5; | ||||||
|  |         numNav.setLayout(rowLayout); | ||||||
|  | 
 | ||||||
|  |         if (numberOfPages > 1) { | ||||||
|  |             if (pageNumber > 1) { | ||||||
|  |                 createRewardLabel(pageNumber, numNav); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             for (int i = pageNumber - this.pageNavSize; i < pageNumber + this.pageNavSize; i++) { | ||||||
|  |                 if (i >= 1 && i <= numberOfPages) { | ||||||
|  |                     createPageNumberLabel(i, i != pageNumber, numNav); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (pageNumber < numberOfPages) { | ||||||
|  |                 createForwardLabel(pageNumber, numNav); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.layout(); | ||||||
|  | 
 | ||||||
|  |         return pageData; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void createPagingHeader(final int page, final int of) { | ||||||
|  |         final Label pageHeader = new Label(this, SWT.NONE); | ||||||
|  |         pageHeader.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); | ||||||
|  |         pageHeader.setText("Page " + page + "/" + of); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void createPageNumberLabel(final int page, final boolean selectable, final Composite parent) { | ||||||
|  |         final Label pageLabel = new Label(parent, SWT.NONE); | ||||||
|  | 
 | ||||||
|  |         pageLabel.setText(" " + String.valueOf(page) + " "); | ||||||
|  |         if (selectable) { | ||||||
|  |             pageLabel.setData(RWT.CUSTOM_VARIANT, "action"); | ||||||
|  |             pageLabel.addListener(SWT.MouseDown, event -> { | ||||||
|  |                 this.entityTable.selectPage(page); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void createForwardLabel(final int pageNumber, final Composite parent) { | ||||||
|  |         final Label forward = new Label(parent, SWT.NONE); | ||||||
|  |         forward.setText(">"); | ||||||
|  |         forward.setData(RWT.CUSTOM_VARIANT, "action"); | ||||||
|  |         forward.addListener(SWT.MouseDown, event -> { | ||||||
|  |             this.entityTable.selectPage(pageNumber + 1); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void createRewardLabel(final int pageNumber, final Composite parent) { | ||||||
|  |         final Label reward = new Label(parent, SWT.NONE); | ||||||
|  |         reward.setText("<"); | ||||||
|  |         reward.setData(RWT.CUSTOM_VARIANT, "action"); | ||||||
|  |         reward.addListener(SWT.MouseDown, event -> { | ||||||
|  |             this.entityTable.selectPage(pageNumber - 1); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,13 @@ | ||||||
|  | /* | ||||||
|  |  * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) | ||||||
|  |  *  | ||||||
|  |  * This Source Code Form is subject to the terms of the Mozilla Public | ||||||
|  |  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
|  |  * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | package ch.ethz.seb.sebserver.gui.service.table; | ||||||
|  | 
 | ||||||
|  | public class TableRowAction { | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -8,8 +8,7 @@ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.gui.service.widget; | package ch.ethz.seb.sebserver.gui.service.widget; | ||||||
| 
 | 
 | ||||||
| import static ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService.POLYGLOT_TREE_ITEM_TEXT_DATA_KEY; | import static ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService.*; | ||||||
| import static ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService.POLYGLOT_WIDGET_FUNCTION_KEY; |  | ||||||
| 
 | 
 | ||||||
| import java.io.InputStream; | import java.io.InputStream; | ||||||
| import java.util.Iterator; | import java.util.Iterator; | ||||||
|  | @ -39,12 +38,17 @@ import org.slf4j.LoggerFactory; | ||||||
| import org.springframework.context.annotation.Lazy; | import org.springframework.context.annotation.Lazy; | ||||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||||
| 
 | 
 | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.Entity; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.Page; | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; | import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Tuple; | import ch.ethz.seb.sebserver.gbl.util.Tuple; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.RWTUtils; | ||||||
| import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; | import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; | ||||||
| import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; | import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; | ||||||
| import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService; | import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService; | ||||||
| import ch.ethz.seb.sebserver.gui.service.page.PageContext; | import ch.ethz.seb.sebserver.gui.service.page.PageContext; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; | ||||||
|  | import ch.ethz.seb.sebserver.gui.service.table.TableBuilder; | ||||||
| 
 | 
 | ||||||
| @Lazy | @Lazy | ||||||
| @Service | @Service | ||||||
|  | @ -158,6 +162,12 @@ public class WidgetFactory { | ||||||
|         return label; |         return label; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public Label labelLocalizedTitle(final Composite content, final LocTextKey locTextKey) { | ||||||
|  |         final Label labelLocalized = labelLocalized(content, RWTUtils.TEXT_NAME_H2, locTextKey); | ||||||
|  |         labelLocalized.setLayoutData(new GridData(SWT.TOP, SWT.LEFT, true, false)); | ||||||
|  |         return labelLocalized; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public Tree treeLocalized(final Composite parent, final int style) { |     public Tree treeLocalized(final Composite parent, final int style) { | ||||||
|         final Tree tree = new Tree(parent, SWT.SINGLE | SWT.FULL_SELECTION); |         final Tree tree = new Tree(parent, SWT.SINGLE | SWT.FULL_SELECTION); | ||||||
|         this.injectI18n(tree); |         this.injectI18n(tree); | ||||||
|  | @ -188,15 +198,23 @@ public class WidgetFactory { | ||||||
|         return item; |         return item; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public <T extends Entity> TableBuilder<T> entityTableBuilder(final RestCall<Page<T>> apiCall) { | ||||||
|  |         return new TableBuilder<>(this, apiCall); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public Table tableLocalized(final Composite parent) { |     public Table tableLocalized(final Composite parent) { | ||||||
|         final Table table = new Table(parent, SWT.NONE); |         final Table table = new Table(parent, SWT.SINGLE | SWT.NO_SCROLL); | ||||||
|         this.injectI18n(table); |         this.injectI18n(table); | ||||||
|         return table; |         return table; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public TableColumn tableColumnLocalized(final Table table, final String locTextKey) { |     public TableColumn tableColumnLocalized( | ||||||
|  |             final Table table, | ||||||
|  |             final LocTextKey locTextKey, | ||||||
|  |             final LocTextKey toolTipKey) { | ||||||
|  | 
 | ||||||
|         final TableColumn tableColumn = new TableColumn(table, SWT.NONE); |         final TableColumn tableColumn = new TableColumn(table, SWT.NONE); | ||||||
|         this.injectI18n(tableColumn, new LocTextKey(locTextKey)); |         this.injectI18n(tableColumn, locTextKey, toolTipKey); | ||||||
|         return tableColumn; |         return tableColumn; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -322,9 +340,14 @@ public class WidgetFactory { | ||||||
|         table.setData(POLYGLOT_WIDGET_FUNCTION_KEY, tableFunction(this.i18nSupport)); |         table.setData(POLYGLOT_WIDGET_FUNCTION_KEY, tableFunction(this.i18nSupport)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void injectI18n(final TableColumn tableColumn, final LocTextKey locTextKey) { |     public void injectI18n(final TableColumn tableColumn, final LocTextKey locTextKey, final LocTextKey locTooltipKey) { | ||||||
|         tableColumn.setData(POLYGLOT_TREE_ITEM_TEXT_DATA_KEY, locTextKey); |         tableColumn.setData(POLYGLOT_TREE_ITEM_TEXT_DATA_KEY, locTextKey); | ||||||
|         tableColumn.setText(this.i18nSupport.getText(locTextKey)); |         tableColumn.setText(this.i18nSupport.getText(locTextKey)); | ||||||
|  | 
 | ||||||
|  |         if (locTooltipKey != null) { | ||||||
|  |             tableColumn.setData(POLYGLOT_TREE_ITEM_TOOLTIP_DATA_KEY, locTooltipKey); | ||||||
|  |             tableColumn.setToolTipText(this.i18nSupport.getText(locTooltipKey)); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void injectI18n(final TableItem tableItem, final LocTextKey... locTextKey) { |     public void injectI18n(final TableItem tableItem, final LocTextKey... locTextKey) { | ||||||
|  |  | ||||||
|  | @ -27,6 +27,7 @@ import ch.ethz.seb.sebserver.gbl.model.Page; | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport; | import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.InstitutionRecordDynamicSqlSupport; | ||||||
| import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserActivityLogRecordDynamicSqlSupport; | import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserActivityLogRecordDynamicSqlSupport; | ||||||
| import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserRecordDynamicSqlSupport; | import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserRecordDynamicSqlSupport; | ||||||
| 
 | 
 | ||||||
|  | @ -36,29 +37,26 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserRecordDynamic | ||||||
| public class PaginationService { | public class PaginationService { | ||||||
| 
 | 
 | ||||||
|     public enum SortOrder { |     public enum SortOrder { | ||||||
|         ASCENDING("+"), |         ASCENDING, | ||||||
|         DESCENDING("-"); |         DESCENDING; | ||||||
| 
 | 
 | ||||||
|         public final String prefix; |         public final static String DESCENDING_PREFIX = "-"; | ||||||
| 
 | 
 | ||||||
|         private SortOrder(final String prefix) { |         public String encode(final String sort) { | ||||||
|             this.prefix = prefix; |             return (this == DESCENDING) ? DESCENDING_PREFIX + sort : sort; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public static SortOrder getSortOrder(final String sort) { |         public static String decode(final String sort) { | ||||||
|             return (sort != null && sort.startsWith(DESCENDING.prefix)) |             return (sort != null && sort.startsWith(DESCENDING_PREFIX)) | ||||||
|                     ? SortOrder.DESCENDING |  | ||||||
|                     : SortOrder.ASCENDING; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         public static String getSortColumn(final String sort) { |  | ||||||
|             return (sort == null) |  | ||||||
|                     ? null |  | ||||||
|                     : (sort.startsWith(SortOrder.ASCENDING.prefix) || sort.startsWith(SortOrder.DESCENDING.prefix)) |  | ||||||
|                     ? sort.substring(1) |                     ? sort.substring(1) | ||||||
|                     : sort; |                     : sort; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         public static SortOrder getSortOrder(final String encoded) { | ||||||
|  |             return (encoded != null && encoded.startsWith(DESCENDING_PREFIX)) | ||||||
|  |                     ? SortOrder.DESCENDING | ||||||
|  |                     : SortOrder.ASCENDING; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private final int defaultPageSize; |     private final int defaultPageSize; | ||||||
|  | @ -137,10 +135,11 @@ public class PaginationService { | ||||||
|         final com.github.pagehelper.Page<Object> startPage = |         final com.github.pagehelper.Page<Object> startPage = | ||||||
|                 PageHelper.startPage(getPageNumber(pageNumber), getPageSize(pageSize), true, true, false); |                 PageHelper.startPage(getPageNumber(pageNumber), getPageSize(pageSize), true, true, false); | ||||||
| 
 | 
 | ||||||
|         if (table != null) { |         if (table != null && StringUtils.isNoneBlank(sort)) { | ||||||
|  |             final SortOrder sortOrder = SortOrder.getSortOrder(sort); | ||||||
|             final String sortColumnName = verifySortColumnName(sort, table); |             final String sortColumnName = verifySortColumnName(sort, table); | ||||||
|             if (StringUtils.isNoneBlank(sortColumnName)) { |             if (StringUtils.isNoneBlank(sortColumnName)) { | ||||||
|                 switch (SortOrder.getSortOrder(sort)) { |                 switch (sortOrder) { | ||||||
|                     case DESCENDING: { |                     case DESCENDING: { | ||||||
|                         PageHelper.orderBy(sortColumnName + " DESC"); |                         PageHelper.orderBy(sortColumnName + " DESC"); | ||||||
|                         break; |                         break; | ||||||
|  | @ -178,7 +177,7 @@ public class PaginationService { | ||||||
| 
 | 
 | ||||||
|         final Map<String, String> mapping = this.sortColumnMapping.get(table.name()); |         final Map<String, String> mapping = this.sortColumnMapping.get(table.name()); | ||||||
|         if (mapping != null) { |         if (mapping != null) { | ||||||
|             final String sortColumn = SortOrder.getSortColumn(sort); |             final String sortColumn = SortOrder.decode(sort); | ||||||
|             if (StringUtils.isBlank(sortColumn)) { |             if (StringUtils.isBlank(sortColumn)) { | ||||||
|                 return this.defaultSortColumn.get(table.name()); |                 return this.defaultSortColumn.get(table.name()); | ||||||
|             } |             } | ||||||
|  | @ -191,6 +190,24 @@ public class PaginationService { | ||||||
|     // TODO is it possible to generate this within MyBatis generator? |     // TODO is it possible to generate this within MyBatis generator? | ||||||
|     private void initSortColumnMapping() { |     private void initSortColumnMapping() { | ||||||
| 
 | 
 | ||||||
|  |         // Institution Table | ||||||
|  |         final Map<String, String> institutionTableMap = new HashMap<>(); | ||||||
|  |         institutionTableMap.put( | ||||||
|  |                 Domain.INSTITUTION.ATTR_NAME, | ||||||
|  |                 InstitutionRecordDynamicSqlSupport.name.name()); | ||||||
|  |         institutionTableMap.put( | ||||||
|  |                 Domain.INSTITUTION.ATTR_URL_SUFFIX, | ||||||
|  |                 InstitutionRecordDynamicSqlSupport.urlSuffix.name()); | ||||||
|  |         institutionTableMap.put( | ||||||
|  |                 Domain.INSTITUTION.ATTR_ACTIVE, | ||||||
|  |                 InstitutionRecordDynamicSqlSupport.active.name()); | ||||||
|  |         this.sortColumnMapping.put( | ||||||
|  |                 InstitutionRecordDynamicSqlSupport.institutionRecord.name(), | ||||||
|  |                 institutionTableMap); | ||||||
|  |         this.defaultSortColumn.put( | ||||||
|  |                 InstitutionRecordDynamicSqlSupport.institutionRecord.name(), | ||||||
|  |                 Domain.INSTITUTION.ATTR_ID); | ||||||
|  | 
 | ||||||
|         // User Table |         // User Table | ||||||
|         final Map<String, String> userTableMap = new HashMap<>(); |         final Map<String, String> userTableMap = new HashMap<>(); | ||||||
|         userTableMap.put(Domain.USER.ATTR_NAME, UserRecordDynamicSqlSupport.name.name()); |         userTableMap.put(Domain.USER.ATTR_NAME, UserRecordDynamicSqlSupport.name.name()); | ||||||
|  |  | ||||||
|  | @ -107,7 +107,7 @@ final class MockupLmsAPITemplate implements LmsAPITemplate { | ||||||
|                 ? -1 |                 ? -1 | ||||||
|                 : 1; |                 : 1; | ||||||
| 
 | 
 | ||||||
|         final String _sort = SortOrder.getSortColumn(sort); |         final String _sort = SortOrder.decode(sort); | ||||||
|         final Comparator<QuizData> comp = (_sort != null) |         final Comparator<QuizData> comp = (_sort != null) | ||||||
|                 ? (_sort.equals(QuizData.FILTER_ATTR_START_TIME)) |                 ? (_sort.equals(QuizData.FILTER_ATTR_START_TIME)) | ||||||
|                         ? (q1, q2) -> q1.startTime.compareTo(q2.startTime) * orderFactor |                         ? (q1, q2) -> q1.startTime.compareTo(q2.startTime) * orderFactor | ||||||
|  |  | ||||||
|  | @ -30,9 +30,9 @@ import org.springframework.web.bind.annotation.RequestMethod; | ||||||
| import org.springframework.web.bind.annotation.RequestParam; | import org.springframework.web.bind.annotation.RequestParam; | ||||||
| import org.springframework.web.bind.annotation.RestController; | import org.springframework.web.bind.annotation.RestController; | ||||||
| 
 | 
 | ||||||
| import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.api.POSTMapper; | import ch.ethz.seb.sebserver.gbl.api.POSTMapper; | ||||||
| import ch.ethz.seb.sebserver.gbl.api.SEBServerRestEndpoints; | import ch.ethz.seb.sebserver.gbl.api.SEBServerRestEndpoints; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.Entity; | import ch.ethz.seb.sebserver.gbl.model.Entity; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.EntityKey; | import ch.ethz.seb.sebserver.gbl.model.EntityKey; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.EntityType; | import ch.ethz.seb.sebserver.gbl.model.EntityType; | ||||||
|  | @ -137,7 +137,7 @@ public class ExamAdministrationController extends ActivatableEntityController<Ex | ||||||
|                     this.examDAO.allMatching(new FilterMap(allRequestParams)).getOrThrow()); |                     this.examDAO.allMatching(new FilterMap(allRequestParams)).getOrThrow()); | ||||||
| 
 | 
 | ||||||
|             if (!StringUtils.isBlank(sort)) { |             if (!StringUtils.isBlank(sort)) { | ||||||
|                 final String sortBy = SortOrder.getSortColumn(sort); |                 final String sortBy = SortOrder.decode(sort); | ||||||
|                 if (sortBy.equals(QuizData.QUIZ_ATTR_NAME)) { |                 if (sortBy.equals(QuizData.QUIZ_ATTR_NAME)) { | ||||||
|                     Collections.sort(exams, (exam1, exam2) -> exam1.name.compareTo(exam2.name)); |                     Collections.sort(exams, (exam1, exam2) -> exam1.name.compareTo(exam2.name)); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  | @ -12,9 +12,7 @@ import java.util.Collection; | ||||||
| 
 | 
 | ||||||
| import javax.servlet.http.HttpServletRequest; | import javax.servlet.http.HttpServletRequest; | ||||||
| 
 | 
 | ||||||
| import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; |  | ||||||
| import org.springframework.context.ApplicationEvent; | import org.springframework.context.ApplicationEvent; | ||||||
| import org.springframework.context.annotation.Import; |  | ||||||
| import org.springframework.context.event.EventListener; | import org.springframework.context.event.EventListener; | ||||||
| import org.springframework.http.HttpStatus; | import org.springframework.http.HttpStatus; | ||||||
| import org.springframework.security.oauth2.common.OAuth2AccessToken; | import org.springframework.security.oauth2.common.OAuth2AccessToken; | ||||||
|  | @ -29,7 +27,6 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| 
 | 
 | ||||||
| @Controller | @Controller | ||||||
| @WebServiceProfile | @WebServiceProfile | ||||||
| @Import(DataSourceAutoConfiguration.class) |  | ||||||
| public class RevokeTokenEndpoint { | public class RevokeTokenEndpoint { | ||||||
| 
 | 
 | ||||||
|     private final ConsumerTokenServices tokenServices; |     private final ConsumerTokenServices tokenServices; | ||||||
|  |  | ||||||
|  | @ -18,8 +18,6 @@ sebserver.webservice.api.exam.accessTokenValiditySeconds=1800 | ||||||
| sebserver.webservice.api.exam.refreshTokenValiditySeconds=-1 | sebserver.webservice.api.exam.refreshTokenValiditySeconds=-1 | ||||||
| sebserver.webservice.api.redirect.unauthorized=http://=0.0.0.0:8080/gui | sebserver.webservice.api.redirect.unauthorized=http://=0.0.0.0:8080/gui | ||||||
| 
 | 
 | ||||||
| sebserver.webservice.api.redirect.unauthorized=none |  | ||||||
| 
 |  | ||||||
| server.servlet.session.cookie.http-only=true | server.servlet.session.cookie.http-only=true | ||||||
| server.servlet.session.tracking-modes=cookie | server.servlet.session.tracking-modes=cookie | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -29,3 +29,13 @@ sebserver.actionpane.title=Actions | ||||||
| sebserver.activities.inst=Institution | sebserver.activities.inst=Institution | ||||||
| 
 | 
 | ||||||
| sebserver.error.unexpected=Unexpected Error | sebserver.error.unexpected=Unexpected Error | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ################################ | ||||||
|  | # Institution | ||||||
|  | ################################ | ||||||
|  | 
 | ||||||
|  | sebserver.institution.list.title=Institutions | ||||||
|  | sebserver.institution.list.column.name=Name | ||||||
|  | sebserver.institution.list.column.urlSuffix=URL Suffix | ||||||
|  | sebserver.institution.list.column.active=Active | ||||||
|  | @ -26,6 +26,21 @@ Label { | ||||||
|     text-shadow: none; |     text-shadow: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | Label.action { | ||||||
|  |     font: 12px Verdana, "Lucida Sans", Arial, Helvetica, sans-serif; | ||||||
|  |     color: #82BE1E; | ||||||
|  |     background-color: transparent; | ||||||
|  |     background-image: none; | ||||||
|  |     background-repeat: repeat; | ||||||
|  |     background-position: left top; | ||||||
|  |     border: none; | ||||||
|  |     border-radius: 0; | ||||||
|  |     text-decoration: none; | ||||||
|  |     cursor: default; | ||||||
|  |     opacity: 1; | ||||||
|  |     text-shadow: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| Label.h1 { | Label.h1 { | ||||||
|     font: 25px Arial, Helvetica, sans-serif; |     font: 25px Arial, Helvetica, sans-serif; | ||||||
|     height: 28px; |     height: 28px; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 anhefti
						anhefti