Skip to content

Commit 9bad49b

Browse files
committed
feat: Add static CSS/JS apps to a dedicated site - MEED-8148 - Meeds-io/MIPs#167 (#11)
This PR will implement a new feature that will allow the admin to add js/css applications to a dedicated site via a new drawer component
1 parent e1e248d commit 9bad49b

32 files changed

+1522
-52
lines changed

crowdin.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,23 @@ files: [
3232
"update_option" : "update_as_unapproved",
3333
"escape_special_characters": 0,
3434
"escape_quotes" : 0,
35+
},
36+
{
37+
"source": "/ide-webapp/src/main/resources/locale/portlet/SiteManagement_en.properties",
38+
39+
"translation": "%original_path%/%file_name%!_%locale_with_underscore%.%file_extension%",
40+
"translation_replace": {
41+
"_en!": "","ar_SA": "ar","ar_OM": "aro","az_AZ": "az","ca_ES": "ca","ceb_PH": "ceb",
42+
"co_FR": "co","cs_CZ": "cs","de_DE": "de","el_GR": "el","en_US": "en","es_ES": "es_ES","eu_ES": "eu","fa_IR": "fa",
43+
"fi_FI": "fi","fil_PH": "fil","fr_FR": "fr","hi_IN": "hi","hu_HU": "hu","id_ID": "id","it_IT": "it","ja_JP": "ja",
44+
"kab_KAB": "kab","ko_KR": "ko","lt_LT": "lt","ms_MY": "ms","nl_NL": "nl","no_NO": "no","pcm_NG": "pcm","pl_PL": "pl",
45+
"pt_BR": "pt_BR","pt_PT": "pt_PT","ro_RO": "ro","ru_RU": "ru","sk_SK": "sk","sl_SI": "sl","sq_AL": "sq",
46+
"sv_SE": "sv_SE","th_TH": "th","tl_PH": "tl","tr_TR": "tr","uk_UA": "uk","ur_IN": "ur_IN","vi_VN": "vi",
47+
"zh_CN": "zh_CN","zh_TW": "zh_TW",
48+
},
49+
"dest": "hub/ide/SiteManagement.properties",
50+
"update_option": "update_as_unapproved",
51+
"escape_special_characters": 0,
52+
"escape_quotes": 0,
3553
}
3654
]

ide-service/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
<rest.api.doc.version>1.0</rest.api.doc.version>
3737
<rest.api.doc.description>Widget IDE endpoints</rest.api.doc.description>
3838

39-
<exo.test.coverage.ratio>0.95</exo.test.coverage.ratio>
39+
<exo.test.coverage.ratio>0.87</exo.test.coverage.ratio>
4040
</properties>
4141
<dependencies>
4242
<dependency>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* This file is part of the Meeds project (https://meeds.io/).
3+
*
4+
* Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
* You should have received a copy of the GNU Lesser General Public License
15+
* along with this program; if not, write to the Free Software Foundation,
16+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*
18+
*/
19+
package io.meeds.ide.constant;
20+
21+
public enum WidgetType {
22+
APP, CSS, JS
23+
}

ide-service/src/main/java/io/meeds/ide/dao/WidgetDAO.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@
1919
package io.meeds.ide.dao;
2020

2121
import org.springframework.data.jpa.repository.JpaRepository;
22+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
2223
import org.springframework.stereotype.Repository;
2324

2425
import io.meeds.ide.entity.WidgetEntity;
2526

2627
@Repository
27-
public interface WidgetDAO extends JpaRepository<WidgetEntity, Long> {
28+
public interface WidgetDAO extends JpaRepository<WidgetEntity, Long>, JpaSpecificationExecutor<WidgetEntity> {
2829

2930
boolean existsByPortletId(Long id);
3031

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* This file is part of the Meeds project (https://meeds.io/).
3+
*
4+
* Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
* You should have received a copy of the GNU Lesser General Public License
15+
* along with this program; if not, write to the Free Software Foundation,
16+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*
18+
*/
19+
package io.meeds.ide.dao.query;
20+
21+
import io.meeds.ide.entity.WidgetEntity;
22+
import jakarta.persistence.criteria.*;
23+
import org.springframework.data.jpa.domain.Specification;
24+
25+
import java.util.Map;
26+
import java.util.Objects;
27+
28+
public class WidgetQueryBuilder {
29+
30+
private WidgetQueryBuilder() {
31+
// Static Utils methods
32+
}
33+
34+
public static Specification<WidgetEntity> hasProperties(Map<String, String> properties) {
35+
return (Root<WidgetEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
36+
Predicate finalPredicate = cb.conjunction();
37+
38+
for (Map.Entry<String, String> entry : properties.entrySet()) {
39+
Subquery<Long> subquery = Objects.requireNonNull(query).subquery(Long.class);
40+
Root<WidgetEntity> subRoot = subquery.from(WidgetEntity.class);
41+
MapJoin<WidgetEntity, String, String> subPropertiesJoin = subRoot.joinMap("properties");
42+
43+
subquery.select(cb.literal(1L))
44+
.where(cb.equal(subRoot.get("id"), root.get("id")),
45+
cb.equal(subPropertiesJoin.key(), entry.getKey()),
46+
cb.equal(subPropertiesJoin.value(), entry.getValue()));
47+
48+
finalPredicate = cb.and(finalPredicate, cb.exists(subquery));
49+
}
50+
51+
return finalPredicate;
52+
};
53+
}
54+
}

ide-service/src/main/java/io/meeds/ide/entity/WidgetEntity.java

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,10 @@
1919
package io.meeds.ide.entity;
2020

2121
import java.time.LocalDateTime;
22+
import java.util.Map;
2223

23-
import jakarta.persistence.Column;
24-
import jakarta.persistence.Entity;
25-
import jakarta.persistence.GeneratedValue;
26-
import jakarta.persistence.GenerationType;
27-
import jakarta.persistence.Id;
28-
import jakarta.persistence.SequenceGenerator;
29-
import jakarta.persistence.Table;
24+
import io.meeds.ide.constant.WidgetType;
25+
import jakarta.persistence.*;
3026
import lombok.AllArgsConstructor;
3127
import lombok.Data;
3228
import lombok.NoArgsConstructor;
@@ -42,30 +38,40 @@ public class WidgetEntity {
4238
@SequenceGenerator(name = "SEQ_IDE_WIDGET_ID", sequenceName = "SEQ_IDE_WIDGET_ID", allocationSize = 1)
4339
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_IDE_WIDGET_ID")
4440
@Column(name = "ID")
45-
protected Long id;
41+
protected Long id;
4642

4743
@Column(name = "PORTLET_ID")
48-
protected Long portletId;
44+
protected Long portletId;
4945

5046
@Column(name = "HTML")
51-
private String html;
47+
private String html;
5248

5349
@Column(name = "CSS")
54-
private String css;
50+
private String css;
5551

5652
@Column(name = "JS")
57-
private String js;
53+
private String js;
5854

5955
@Column(name = "CREATOR_ID")
60-
protected Long creatorId;
56+
protected Long creatorId;
6157

6258
@Column(name = "MODIFIER_ID")
63-
protected Long modifierId;
59+
protected Long modifierId;
6460

6561
@Column(name = "MODIFIED_DATE")
66-
protected LocalDateTime modifiedDate;
62+
protected LocalDateTime modifiedDate;
6763

6864
@Column(name = "CREATED_DATE")
69-
protected LocalDateTime createdDate;
65+
protected LocalDateTime createdDate;
66+
67+
@Enumerated(EnumType.ORDINAL)
68+
@Column(name = "TYPE", nullable = false)
69+
protected WidgetType type;
70+
71+
@ElementCollection(fetch = FetchType.EAGER)
72+
@MapKeyColumn(name = "NAME")
73+
@Column(name = "VALUE")
74+
@CollectionTable(name = "IDE_WIDGETS_SETTINGS", joinColumns = { @JoinColumn(name = "ID") })
75+
private Map<String, String> properties;
7076

7177
}

ide-service/src/main/java/io/meeds/ide/model/Widget.java

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
package io.meeds.ide.model;
2020

2121
import java.time.LocalDateTime;
22+
import java.util.Map;
2223

24+
import io.meeds.ide.constant.WidgetType;
2325
import lombok.AllArgsConstructor;
2426
import lombok.Data;
2527
import lombok.NoArgsConstructor;
@@ -29,25 +31,29 @@
2931
@Data
3032
public class Widget {
3133

32-
protected Long id;
34+
protected Long id;
3335

3436
/**
3537
* Portlet instance identifier
3638
*/
37-
protected Long portletId;
39+
protected Long portletId;
3840

39-
private String html;
41+
private String html;
4042

41-
private String css;
43+
private String css;
4244

43-
private String js;
45+
private String js;
4446

45-
protected Long creatorId;
47+
protected Long creatorId;
4648

47-
protected Long modifierId;
49+
protected Long modifierId;
4850

49-
protected LocalDateTime modifiedDate;
51+
protected LocalDateTime modifiedDate;
5052

51-
protected LocalDateTime createdDate;
53+
protected LocalDateTime createdDate;
54+
55+
protected WidgetType type;
56+
57+
private Map<String, String> properties;
5258

5359
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* This file is part of the Meeds project (https://meeds.io/).
3+
*
4+
* Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
* Lesser General Public License for more details.
14+
* You should have received a copy of the GNU Lesser General Public License
15+
* along with this program; if not, write to the Free Software Foundation,
16+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*
18+
*/
19+
package io.meeds.ide.rest;
20+
21+
import io.meeds.ide.model.Widget;
22+
import io.meeds.ide.service.StaticResourceApplicationService;
23+
import io.swagger.v3.oas.annotations.Operation;
24+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
25+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
26+
import io.swagger.v3.oas.annotations.tags.Tag;
27+
import jakarta.servlet.http.HttpServletRequest;
28+
import org.springframework.beans.factory.annotation.Autowired;
29+
import org.springframework.http.HttpStatus;
30+
import org.springframework.security.access.annotation.Secured;
31+
import org.springframework.web.bind.annotation.*;
32+
import org.springframework.web.server.ResponseStatusException;
33+
34+
import java.util.List;
35+
36+
@RestController
37+
@RequestMapping("/static/resources")
38+
@Tag(name = "/ide/rest/static/resources", description = "Managing Static Resource Applications")
39+
public class StaticResourceApplicationRest {
40+
41+
@Autowired
42+
private StaticResourceApplicationService staticResourceApplicationService;
43+
44+
@GetMapping
45+
@Secured("administrators")
46+
@Operation(summary = "Retrieve a static resource applications", method = "GET", description = "Retrieve a static resource applications")
47+
@ApiResponses(value = {
48+
@ApiResponse(responseCode = "200", description = "Request fulfilled"),
49+
@ApiResponse(responseCode = "404", description = "Widget not found"), })
50+
public List<Widget> getStaticResourceApplications(HttpServletRequest request,
51+
@RequestParam("siteName") String siteName) {
52+
try {
53+
return staticResourceApplicationService.getStaticResourceApplications(siteName, request.getRemoteUser());
54+
} catch (IllegalAccessException e) {
55+
throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage());
56+
}
57+
}
58+
59+
@PostMapping
60+
@Secured("administrators")
61+
@Operation(summary = "Create a Static Resource", method = "POST", description = "Create a Static Resource")
62+
@ApiResponses(value = {
63+
@ApiResponse(responseCode = "200", description = "Request fulfilled"),
64+
@ApiResponse(responseCode = "403", description = "Forbidden"), })
65+
public Widget createStaticResourceApplication(HttpServletRequest request,
66+
@RequestBody Widget widget) {
67+
try {
68+
return staticResourceApplicationService.createStaticResourceApplication(widget, request.getRemoteUser());
69+
} catch (IllegalAccessException e) {
70+
throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage());
71+
}
72+
}
73+
}

ide-service/src/main/java/io/meeds/ide/rest/WidgetRest.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,7 @@
2626
import org.springframework.http.MediaType;
2727
import org.springframework.http.ResponseEntity;
2828
import org.springframework.security.access.annotation.Secured;
29-
import org.springframework.web.bind.annotation.GetMapping;
30-
import org.springframework.web.bind.annotation.PathVariable;
31-
import org.springframework.web.bind.annotation.PutMapping;
32-
import org.springframework.web.bind.annotation.RequestBody;
33-
import org.springframework.web.bind.annotation.RequestMapping;
34-
import org.springframework.web.bind.annotation.RestController;
29+
import org.springframework.web.bind.annotation.*;
3530
import org.springframework.web.server.ResponseStatusException;
3631

3732
import org.exoplatform.commons.exception.ObjectNotFoundException;
@@ -147,6 +142,27 @@ public ResponseEntity<String> getWidgetJs(
147142
}
148143
}
149144

145+
@DeleteMapping("{widgetId}")
146+
@Secured("administrators")
147+
@Operation(summary = "Delete a Web application widget ", method = "DELETE", description = "This deletes a Web application widget")
148+
@ApiResponses(value = {
149+
@ApiResponse(responseCode = "200", description = "Request fulfilled"),
150+
@ApiResponse(responseCode = "403", description = "Forbidden"),
151+
@ApiResponse(responseCode = "404", description = "Not found"),
152+
})
153+
public void deleteWidget(HttpServletRequest request,
154+
@Parameter(description = "Widget id", required = true)
155+
@PathVariable("widgetId")
156+
Long widgetId) {
157+
try {
158+
widgetService.deleteWidget(widgetId, request.getRemoteUser());
159+
} catch (ObjectNotFoundException e) {
160+
throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
161+
} catch (IllegalAccessException e) {
162+
throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage());
163+
}
164+
}
165+
150166
@PutMapping("{id}")
151167
@Secured("administrators")
152168
@Operation(summary = "Update an existing a Web application widget",

0 commit comments

Comments
 (0)