mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-01-24 14:24:47 +08:00
project icon upload & reset
This commit is contained in:
parent
f61ff92fd2
commit
db7a46e6ab
@ -25,6 +25,8 @@ public abstract class HangarController {
|
||||
private HangarConfig hangarConfig;
|
||||
@Autowired
|
||||
private MarkdownService markdownService;
|
||||
@Autowired
|
||||
private TemplateHelper templateHelper;
|
||||
|
||||
protected ModelAndView fillModel(ModelAndView mav) {
|
||||
// helpers
|
||||
@ -36,7 +38,7 @@ public abstract class HangarController {
|
||||
mav.addObject("config", hangarConfig);
|
||||
mav.addObject("markdownService", markdownService);
|
||||
mav.addObject("rand", ThreadLocalRandom.current());
|
||||
mav.addObject("utils", new TemplateHelper(hangarConfig));
|
||||
mav.addObject("utils", templateHelper);
|
||||
|
||||
// alerts
|
||||
if (mav.getModelMap().getAttribute("alerts") == null) {
|
||||
|
@ -18,22 +18,28 @@ import me.minidigger.hangar.model.viewhelpers.ProjectData;
|
||||
import me.minidigger.hangar.model.viewhelpers.ProjectPage;
|
||||
import me.minidigger.hangar.model.viewhelpers.ScopedProjectData;
|
||||
import me.minidigger.hangar.security.annotations.GlobalPermission;
|
||||
import me.minidigger.hangar.security.annotations.ProjectPermission;
|
||||
import me.minidigger.hangar.service.OrgService;
|
||||
import me.minidigger.hangar.service.UserActionLogService;
|
||||
import me.minidigger.hangar.service.UserService;
|
||||
import me.minidigger.hangar.service.pluginupload.ProjectFiles;
|
||||
import me.minidigger.hangar.service.project.FlagService;
|
||||
import me.minidigger.hangar.service.project.PagesSerivce;
|
||||
import me.minidigger.hangar.service.project.ProjectFactory;
|
||||
import me.minidigger.hangar.service.project.ProjectService;
|
||||
import me.minidigger.hangar.util.AlertUtil;
|
||||
import me.minidigger.hangar.util.AlertUtil.AlertType;
|
||||
import me.minidigger.hangar.util.FileUtils;
|
||||
import me.minidigger.hangar.util.HangarException;
|
||||
import me.minidigger.hangar.util.RouteHelper;
|
||||
import me.minidigger.hangar.util.StringUtils;
|
||||
import me.minidigger.hangar.util.TemplateHelper;
|
||||
import me.minidigger.hangar.util.TriFunction;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@ -42,12 +48,20 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@ -67,13 +81,15 @@ public class ProjectsController extends HangarController {
|
||||
private final ProjectFactory projectFactory;
|
||||
private final PagesSerivce pagesSerivce;
|
||||
private final UserActionLogService userActionLogService;
|
||||
private final ProjectFiles projectFiles;
|
||||
private final TemplateHelper templateHelper;
|
||||
private final HangarDao<UserDao> userDao;
|
||||
private final HangarDao<ProjectDao> projectDao;
|
||||
|
||||
private final HttpServletRequest request;
|
||||
|
||||
@Autowired
|
||||
public ProjectsController(HangarConfig hangarConfig, RouteHelper routeHelper, UserService userService, OrgService orgService, FlagService flagService, ProjectService projectService, ProjectFactory projectFactory, PagesSerivce pagesSerivce, UserActionLogService userActionLogService, HangarDao<UserDao> userDao, HangarDao<ProjectDao> projectDao, HttpServletRequest request) {
|
||||
public ProjectsController(HangarConfig hangarConfig, RouteHelper routeHelper, UserService userService, OrgService orgService, FlagService flagService, ProjectService projectService, ProjectFactory projectFactory, PagesSerivce pagesSerivce, UserActionLogService userActionLogService, ProjectFiles projectFiles, TemplateHelper templateHelper, HangarDao<UserDao> userDao, HangarDao<ProjectDao> projectDao, HttpServletRequest request) {
|
||||
this.hangarConfig = hangarConfig;
|
||||
this.routeHelper = routeHelper;
|
||||
this.userService = userService;
|
||||
@ -83,6 +99,8 @@ public class ProjectsController extends HangarController {
|
||||
this.projectFactory = projectFactory;
|
||||
this.pagesSerivce = pagesSerivce;
|
||||
this.userActionLogService = userActionLogService;
|
||||
this.projectFiles = projectFiles;
|
||||
this.templateHelper = templateHelper;
|
||||
this.userDao = userDao;
|
||||
this.projectDao = projectDao;
|
||||
this.request = request;
|
||||
@ -170,10 +188,9 @@ public class ProjectsController extends HangarController {
|
||||
ScopedProjectData sp = projectService.getScopedProjectData(projectData.getProject().getId());
|
||||
mav.addObject("sp", sp);
|
||||
mav.addObject("page", ProjectPage.of(pagesSerivce.getPage(projectData.getProject().getId(), hangarConfig.pages.home.getName())));
|
||||
mav.addObject("parentPage");
|
||||
mav.addObject("parentPage"); // TODO parent page
|
||||
mav.addObject("editorOpen", false);
|
||||
pagesSerivce.fillPages(mav, projectData.getProject().getId());
|
||||
// TODO implement show request controller
|
||||
return fillModel(mav);
|
||||
}
|
||||
|
||||
@ -212,25 +229,73 @@ public class ProjectsController extends HangarController {
|
||||
}
|
||||
|
||||
@Secured("ROLE_USER")
|
||||
@PostMapping("/{author}/{slug}/icon")
|
||||
public Object uploadIcon(@PathVariable Object author, @PathVariable Object slug) {
|
||||
return null; // TODO implement uploadIcon request controller
|
||||
@ProjectPermission(NamedPermission.EDIT_SUBJECT_SETTINGS)
|
||||
@PostMapping(value = "/{author}/{slug}/icon", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public Object uploadIcon(@PathVariable String author, @PathVariable String slug, @RequestParam MultipartFile icon, RedirectAttributes attributes) {
|
||||
ProjectsTable project = projectService.getProjectData(author, slug).getProject();
|
||||
if (icon.getContentType() == null || (!icon.getContentType().equals(MediaType.IMAGE_PNG_VALUE) && !icon.getContentType().equals(MediaType.IMAGE_JPEG_VALUE))) {
|
||||
AlertUtil.showAlert(attributes, AlertType.ERROR, "error.invalidFile");
|
||||
return new ModelAndView("redirect:" + routeHelper.getRouteUrl("projects.showSettings", author, slug));
|
||||
}
|
||||
if (icon.getOriginalFilename() == null || icon.getOriginalFilename().isBlank()) {
|
||||
AlertUtil.showAlert(attributes, AlertType.ERROR, "error.noFile");
|
||||
return new ModelAndView("redirect:" + routeHelper.getRouteUrl("projects.showSettings", author, slug));
|
||||
}
|
||||
try {
|
||||
Path pendingDir = projectFiles.getPendingIconDir(author, slug);
|
||||
if (Files.notExists(pendingDir)) {
|
||||
Files.createDirectories(pendingDir);
|
||||
}
|
||||
FileUtils.deletedFiles(pendingDir);
|
||||
Files.copy(icon.getInputStream(), pendingDir.resolve(icon.getOriginalFilename()));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
// todo data
|
||||
userActionLogService.project(request, LoggedActionType.PROJECT_ICON_CHANGED.with(ProjectContext.of(project.getId())), "", "");
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@GetMapping("/{author}/{slug}/icon")
|
||||
public Object showIcon(@PathVariable Object author, @PathVariable Object slug) {
|
||||
return null; // TODO implement showIcon request controller
|
||||
public Object showIcon(@PathVariable String author, @PathVariable String slug) {
|
||||
Path iconPath = projectFiles.getIconPath(author, slug);
|
||||
if (iconPath == null) {
|
||||
return new ModelAndView("redirect:" + templateHelper.avatarUrl(author));
|
||||
}
|
||||
return showImage(iconPath);
|
||||
}
|
||||
|
||||
@GetMapping("/{author}/{slug}/icon/pending")
|
||||
public Object showPendingIcon(@PathVariable Object author, @PathVariable Object slug) {
|
||||
return null; // TODO implement showPendingIcon request controller
|
||||
public ResponseEntity<byte[]> showPendingIcon(@PathVariable String author, @PathVariable String slug) {
|
||||
Path path = projectFiles.getPendingIconPath(author, slug);
|
||||
if (path == null) return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
return showImage(path);
|
||||
}
|
||||
|
||||
private ResponseEntity<byte[]> showImage(Path path) {
|
||||
try {
|
||||
byte[] lastModified = Files.getLastModifiedTime(path).toString().getBytes(StandardCharsets.UTF_8);
|
||||
byte[] lastModifiedHash = MessageDigest.getInstance("MD5").digest(lastModified);
|
||||
String hashString = Base64.getEncoder().encodeToString(lastModifiedHash);
|
||||
return ResponseEntity.ok().header(HttpHeaders.ETAG, hashString).header(HttpHeaders.CACHE_CONTROL, "max-age=" + 3600).body(Files.readAllBytes(path));
|
||||
} catch (IOException | NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Secured("ROLE_USER")
|
||||
@RequestMapping("/{author}/{slug}/icon/reset")
|
||||
public Object resetIcon(@PathVariable Object author, @PathVariable Object slug) {
|
||||
return null; // TODO implement resetIcon request controller
|
||||
@PostMapping("/{author}/{slug}/icon/reset")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public void resetIcon(@PathVariable String author, @PathVariable String slug) {
|
||||
ProjectsTable projectsTable = projectService.getProjectData(author, slug).getProject();
|
||||
Path icon = projectFiles.getIconPath(author, slug);
|
||||
Path pendingIcon = projectFiles.getPendingIconPath(author, slug);
|
||||
FileUtils.delete(icon);
|
||||
FileUtils.delete(pendingIcon);
|
||||
|
||||
// TODO data
|
||||
userActionLogService.project(request, LoggedActionType.PROJECT_ICON_CHANGED.with(ProjectContext.of(projectsTable.getId())), "", "");
|
||||
}
|
||||
|
||||
@Secured("ROLE_USER")
|
||||
@ -241,7 +306,8 @@ public class ProjectsController extends HangarController {
|
||||
mav.addObject("p", projectData);
|
||||
ScopedProjectData scopedProjectData = projectService.getScopedProjectData(projectData.getProject().getId());
|
||||
mav.addObject("sp", scopedProjectData);
|
||||
// TODO add deploymentKey and iconUrl
|
||||
mav.addObject("iconUrl", templateHelper.projectAvatarUrl(projectData.getProject()));
|
||||
// TODO add deploymentKey
|
||||
return fillModel(mav);
|
||||
}
|
||||
|
||||
@ -320,10 +386,28 @@ public class ProjectsController extends HangarController {
|
||||
projectsTable.setForumSync(forumSync);
|
||||
projectsTable.setDescription(description);
|
||||
projectDao.get().update(projectsTable);
|
||||
// TODO update icon handling
|
||||
|
||||
if (updateIcon) {
|
||||
Path pendingIconPath = projectFiles.getPendingIconPath(author, slug);
|
||||
if (pendingIconPath != null) {
|
||||
try {
|
||||
Path iconDir = projectFiles.getIconDir(author, slug);
|
||||
if (Files.notExists(iconDir)) {
|
||||
Files.createDirectories(iconDir);
|
||||
}
|
||||
FileUtils.deletedFiles(iconDir);
|
||||
Files.move(pendingIconPath, iconDir.resolve(pendingIconPath.getFileName()));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO add new roles
|
||||
// TODO update existing roles
|
||||
userActionLogService.project(request, LoggedActionType.PROJECT_SETTINGS_CHANGED.with(ProjectContext.of(projectsTable.getId())), "", "");
|
||||
|
||||
return new RedirectView(routeHelper.getRouteUrl("projects.show", author, slug)); // TODO implement save request controller
|
||||
return new RedirectView(routeHelper.getRouteUrl("projects.show", author, slug));
|
||||
}
|
||||
|
||||
@Secured("ROLE_USER")
|
||||
|
@ -1,30 +1,24 @@
|
||||
package me.minidigger.hangar.service.pluginupload;
|
||||
|
||||
import me.minidigger.hangar.config.hangar.HangarConfig;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import me.minidigger.hangar.config.hangar.HangarConfig;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Component
|
||||
public class ProjectFiles {
|
||||
|
||||
private Path rootDir;
|
||||
private Path publicDir;
|
||||
private Path confDir;
|
||||
private Path uploadsDir;
|
||||
private Path pluginsDir;
|
||||
private Path tmpDir;
|
||||
private final Path pluginsDir;
|
||||
private final Path tmpDir;
|
||||
|
||||
@Autowired
|
||||
public ProjectFiles(HangarConfig hangarConfig) {
|
||||
rootDir = Path.of("");
|
||||
publicDir = rootDir.resolve("public");
|
||||
confDir = rootDir.resolve("conf");
|
||||
uploadsDir = Path.of(hangarConfig.getPluginUploadDir());
|
||||
Path uploadsDir = Path.of(hangarConfig.getPluginUploadDir());
|
||||
pluginsDir = uploadsDir.resolve("plugins");
|
||||
tmpDir = uploadsDir.resolve("tmp");
|
||||
}
|
||||
@ -77,24 +71,13 @@ public class ProjectFiles {
|
||||
}
|
||||
|
||||
private Path findFirstFile(Path dir) {
|
||||
try {
|
||||
return Files.list(dir).filter(Files::isRegularFile).findFirst().orElse(null); // TODO no clue if this is what is desired here
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
// private def findFirstFile(dir: Path): F[Option[Path]] = {
|
||||
// import cats.instances.lazyList._
|
||||
// val findFirst = fileIO.list(dir).use { fs =>
|
||||
// fileIO.traverseLimited(fs)(f => fileIO.isDirectory(f).tupleLeft(f)).map {
|
||||
// _.collectFirst {
|
||||
// case (p, false) => p
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fileIO.exists(dir).ifM(findFirst, F.pure(None))
|
||||
// }
|
||||
// }
|
||||
if (Files.exists(dir)) {
|
||||
try (Stream<Path> pathStream = Files.list(dir)) {
|
||||
return pathStream.filter(Predicate.not(Files::isDirectory)).findFirst().orElse(null);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
} else return null;
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,9 @@ import me.minidigger.hangar.model.viewhelpers.ScopedProjectData;
|
||||
import me.minidigger.hangar.model.viewhelpers.UnhealthyProject;
|
||||
import me.minidigger.hangar.model.viewhelpers.UserRole;
|
||||
import me.minidigger.hangar.service.UserService;
|
||||
import me.minidigger.hangar.service.pluginupload.ProjectFiles;
|
||||
import me.minidigger.hangar.util.StringUtils;
|
||||
import me.minidigger.hangar.util.TemplateHelper;
|
||||
import org.postgresql.shaded.com.ongres.scram.common.util.Preconditions;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@ -36,6 +38,7 @@ import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@ -195,6 +198,7 @@ public class ProjectService {
|
||||
return projectDao.get().getUnhealthyProjects(hangarConfig.projects.getStaleAge().toMillis());
|
||||
}
|
||||
|
||||
// TODO move to API daos
|
||||
public List<Project> getProjects(String pluginId, List<Category> categories, List<Tag> tags, String query, String owner, boolean seeHidden, Long requesterId, ProjectSortingStrategy sort, boolean relevance, long limit, long offset) {
|
||||
String ordering;
|
||||
if (relevance && query != null && !query.isEmpty()) {
|
||||
|
27
src/main/java/me/minidigger/hangar/util/FileUtils.java
Normal file
27
src/main/java/me/minidigger/hangar/util/FileUtils.java
Normal file
@ -0,0 +1,27 @@
|
||||
package me.minidigger.hangar.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class FileUtils {
|
||||
|
||||
private FileUtils() { }
|
||||
|
||||
public static void deletedFiles(Path superDir) {
|
||||
try (Stream<Path> files = Files.list(superDir)) {
|
||||
files.filter(Predicate.not(Files::isDirectory)).forEach(FileUtils::delete);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void delete(Path path) {
|
||||
if (path == null) return;
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException ignored) { }
|
||||
}
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
package me.minidigger.hangar.util;
|
||||
|
||||
import me.minidigger.hangar.config.hangar.HangarConfig;
|
||||
import me.minidigger.hangar.db.model.ProjectsTable;
|
||||
import me.minidigger.hangar.model.viewhelpers.ProjectData;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -8,11 +12,15 @@ import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
|
||||
@Component
|
||||
public class TemplateHelper {
|
||||
|
||||
private final RouteHelper routeHelper;
|
||||
private final HangarConfig hangarConfig;
|
||||
|
||||
public TemplateHelper(HangarConfig hangarConfig) {
|
||||
@Autowired
|
||||
public TemplateHelper(RouteHelper routeHelper, HangarConfig hangarConfig) {
|
||||
this.routeHelper = routeHelper;
|
||||
this.hangarConfig = hangarConfig;
|
||||
}
|
||||
|
||||
@ -28,6 +36,10 @@ public class TemplateHelper {
|
||||
return String.format(hangarConfig.security.api.getAvatarUrl(), name);
|
||||
}
|
||||
|
||||
public String projectAvatarUrl(ProjectsTable table) {
|
||||
return routeHelper.getRouteUrl("projects.showIcon", table.getOwnerName(), table.getSlug());
|
||||
}
|
||||
|
||||
public String formatFileSize(Long size) {
|
||||
if (size < 1024) {
|
||||
return size + "B";
|
||||
|
@ -79,6 +79,7 @@ editor.deleteConfirm = Are you sure you want to delete {0}? This cannot be
|
||||
error.minLength = Content too short.
|
||||
error.maxLength = Content too long.
|
||||
error.noFile = No file submitted.
|
||||
error.invalidFile = That is an invalid file type.
|
||||
error.nameUnavailable = That name is not available.
|
||||
error.noLogin = Login is temporarily unavailable, please try again later.
|
||||
error.loginFailed = Authentication failed.
|
||||
|
@ -8,17 +8,7 @@
|
||||
<#import "*/utils/userAvatar.ftlh" as userAvatar />
|
||||
<#import "*/users/memberList.ftlh" as memberList />
|
||||
|
||||
<#--@import controllers.sugar.Requests.OreRequest
|
||||
@import models.viewhelper.{ProjectData, ScopedProjectData}
|
||||
@import ore.OreConfig
|
||||
@import ore.db.Model
|
||||
@import ore.markdown.MarkdownRenderer
|
||||
@import ore.models.api.ProjectApiKey
|
||||
@import ore.permission.Permission
|
||||
@import util.syntax._
|
||||
@import views.html.helper.{CSPNonce, CSRF, form}
|
||||
@import views.html.utils
|
||||
|
||||
<#--
|
||||
@(p: ProjectData, sp: ScopedProjectData, deploymentKey: Option[Model[ProjectApiKey]], iconUrl: String)(implicit messages: Messages, flash: Flash,
|
||||
request: OreRequest[_], config: OreConfig, renderer: MarkdownRenderer, assetsFinder: AssetsFinder)-->
|
||||
|
||||
@ -39,8 +29,6 @@
|
||||
</script>
|
||||
</#assign>
|
||||
|
||||
<#-- @ftlvariable name="p" type="me.minidigger.hangar.model.viewhelpers.ProjectData" -->
|
||||
<#-- @ftlvariable name="sp" type="me.minidigger.hangar.model.viewhelpers.ScopedProjectData" -->
|
||||
<#assign Permission=@helper["me.minidigger.hangar.model.Permission"]>
|
||||
<@projects.view p=p sp=sp active="#settings" additionalScripts=scriptsVar>
|
||||
|
||||
@ -117,8 +105,7 @@
|
||||
<@csrf.formField />
|
||||
<div class="setting-description">
|
||||
<h4>Icon</h4>
|
||||
<#-- @ftlvariable name="iconUrl" type="java.lang.String" -->
|
||||
<@userAvatar.userAvatar userName=p.projectOwner.name avatarUrl=p.projectOwner.avatarUrl imgSrc=iconUrl clazz="user-avatar-md" />
|
||||
<@userAvatar.userAvatar userName=p.projectOwner.name avatarUrl=utils.avatarUrl(p.projectOwner.name) imgSrc=iconUrl clazz="user-avatar-md" />
|
||||
|
||||
<input class="form-control-static" type="file" id="icon" name="icon" />
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user