add application attachments

This commit is contained in:
东皇 2018-05-17 22:06:23 +08:00
parent 135f3362bb
commit 4986ccd6c7
30 changed files with 753 additions and 188 deletions

View File

@ -9,16 +9,13 @@ import javax.validation.constraints.Max;
import javax.validation.constraints.Min; import javax.validation.constraints.Min;
import me.chyxion.tigon.model.ViewModel; import me.chyxion.tigon.model.ViewModel;
import me.chyxion.tigon.model.ListResult; import me.chyxion.tigon.model.ListResult;
import com.pudonghot.ambition.crm.model.*;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import com.pudonghot.ambition.crm.model.User;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import com.pudonghot.ambition.crm.model.Application;
import org.hibernate.validator.constraints.NotBlank; import org.hibernate.validator.constraints.NotBlank;
import com.pudonghot.ambition.crm.service.UserService; import com.pudonghot.ambition.crm.service.UserService;
import org.apache.shiro.authz.annotation.RequiresRoles; import org.apache.shiro.authz.annotation.RequiresRoles;
import com.pudonghot.ambition.crm.model.CustomerProperty;
import com.pudonghot.ambition.crm.model.ApplicationImage;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import com.pudonghot.ambition.crm.service.ApplicationService; import com.pudonghot.ambition.crm.service.ApplicationService;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@ -28,6 +25,8 @@ import com.pudonghot.ambition.crm.form.create.ApplicationFormForCreate;
import com.pudonghot.ambition.crm.form.update.ApplicationFormForUpdate; import com.pudonghot.ambition.crm.form.update.ApplicationFormForUpdate;
import com.pudonghot.ambition.crm.form.create.ApplicationImageFormForCreate; import com.pudonghot.ambition.crm.form.create.ApplicationImageFormForCreate;
import com.pudonghot.ambition.crm.form.update.ApplicationImageFormForUpdate; import com.pudonghot.ambition.crm.form.update.ApplicationImageFormForUpdate;
import com.pudonghot.ambition.crm.form.create.ApplicationAttachmentFormForCreate;
import com.pudonghot.ambition.crm.form.update.ApplicationAttachmentFormForUpdate;
/** /**
* @author Shaun Chyxion <br> * @author Shaun Chyxion <br>
@ -129,6 +128,26 @@ public class ApplicationController
((ApplicationService) queryService).updateImage(form); ((ApplicationService) queryService).updateImage(form);
} }
@RequestMapping(value = "/add-attachment", method = RequestMethod.POST)
public ViewModel<ApplicationAttachment> addAttachment(
@Valid ApplicationAttachmentFormForCreate form) {
Assert.state(!form.getAttachment().isEmpty(), "Image content is empty");
form.setAdmin(getAuthUser().getUser().getData().isAdmin());
return new ViewModel<>(((ApplicationService) queryService).addAttachment(form));
}
@RequestMapping(value = "/remove-attachment", method = RequestMethod.POST)
public void removeAttachment(@NotBlank @RequestParam("id") String id) {
final User user = getAuthUser().getUser().getData();
((ApplicationService) queryService).removeAttachment(id, user.getId(), user.isAdmin());
}
@RequestMapping(value = "/update-attachment", method = RequestMethod.POST)
public void updateImage(@Valid ApplicationAttachmentFormForUpdate form) {
form.setAdmin(getAuthUser().getUser().getData().isAdmin());
((ApplicationService) queryService).updateAttachment(form);
}
@RequiresRoles(User.ROLE_ADMIN) @RequiresRoles(User.ROLE_ADMIN)
@RequestMapping(value = "/delete", method = RequestMethod.POST) @RequestMapping(value = "/delete", method = RequestMethod.POST)
public void delete(@NotBlank @RequestParam("id") String id) { public void delete(@NotBlank @RequestParam("id") String id) {

View File

@ -2,13 +2,16 @@ package com.pudonghot.ambition.crm.service;
import javax.validation.Valid; import javax.validation.Valid;
import com.pudonghot.ambition.crm.model.Application; import com.pudonghot.ambition.crm.model.Application;
import com.pudonghot.ambition.crm.model.ApplicationImage;
import org.hibernate.validator.constraints.NotBlank; import org.hibernate.validator.constraints.NotBlank;
import me.chyxion.tigon.service.BaseCrudByFormService; import me.chyxion.tigon.service.BaseCrudByFormService;
import com.pudonghot.ambition.crm.model.ApplicationImage;
import com.pudonghot.ambition.crm.model.ApplicationAttachment;
import com.pudonghot.ambition.crm.form.update.ApplicationFormForUpdate; import com.pudonghot.ambition.crm.form.update.ApplicationFormForUpdate;
import com.pudonghot.ambition.crm.form.create.ApplicationFormForCreate; import com.pudonghot.ambition.crm.form.create.ApplicationFormForCreate;
import com.pudonghot.ambition.crm.form.create.ApplicationImageFormForCreate; import com.pudonghot.ambition.crm.form.create.ApplicationImageFormForCreate;
import com.pudonghot.ambition.crm.form.update.ApplicationImageFormForUpdate; import com.pudonghot.ambition.crm.form.update.ApplicationImageFormForUpdate;
import com.pudonghot.ambition.crm.form.create.ApplicationAttachmentFormForCreate;
import com.pudonghot.ambition.crm.form.update.ApplicationAttachmentFormForUpdate;
/** /**
* @author Shaun Chyxion <br> * @author Shaun Chyxion <br>
@ -37,5 +40,26 @@ public interface ApplicationService
* @param form form * @param form form
*/ */
void updateImage(@Valid ApplicationImageFormForUpdate form); void updateImage(@Valid ApplicationImageFormForUpdate form);
/**
* add attachment
* @param form form
*/
ApplicationAttachment addAttachment(@Valid ApplicationAttachmentFormForCreate form);
/**
* remove attachment
* @param id attachment id
* @param userId user id
* @param admin admin
*/
void removeAttachment(@NotBlank String id, String userId, boolean admin);
/**
* update attachment
* @param form form
*/
void updateAttachment(@Valid ApplicationAttachmentFormForUpdate form);
} }

View File

@ -7,10 +7,13 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import me.chyxion.tigon.mybatis.Search; import me.chyxion.tigon.mybatis.Search;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import me.chyxion.tigon.model.ViewModel; import me.chyxion.tigon.model.ViewModel;
import com.pudonghot.ambition.crm.model.*; import com.pudonghot.ambition.crm.model.*;
import org.apache.commons.io.FilenameUtils;
import com.pudonghot.ambition.crm.mapper.*; import com.pudonghot.ambition.crm.mapper.*;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -24,8 +27,11 @@ import org.springframework.transaction.annotation.Transactional;
import com.pudonghot.ambition.crm.service.CustomerPermissionService; import com.pudonghot.ambition.crm.service.CustomerPermissionService;
import com.pudonghot.ambition.crm.form.update.ApplicationFormForUpdate; import com.pudonghot.ambition.crm.form.update.ApplicationFormForUpdate;
import com.pudonghot.ambition.crm.form.create.ApplicationFormForCreate; import com.pudonghot.ambition.crm.form.create.ApplicationFormForCreate;
import com.pudonghot.ambition.crm.form.update.ApplicationFileFormForUpdate;
import com.pudonghot.ambition.crm.form.create.ApplicationImageFormForCreate; import com.pudonghot.ambition.crm.form.create.ApplicationImageFormForCreate;
import com.pudonghot.ambition.crm.form.update.ApplicationImageFormForUpdate; import com.pudonghot.ambition.crm.form.update.ApplicationImageFormForUpdate;
import com.pudonghot.ambition.crm.form.create.ApplicationAttachmentFormForCreate;
import com.pudonghot.ambition.crm.form.update.ApplicationAttachmentFormForUpdate;
/** /**
* @author Shaun Chyxion <br> * @author Shaun Chyxion <br>
@ -41,6 +47,8 @@ public class ApplicationServiceSupport
@Autowired @Autowired
private ApplicationImageMapper imageMapper; private ApplicationImageMapper imageMapper;
@Autowired @Autowired
private ApplicationAttachmentMapper attachmentMapper;
@Autowired
private DiskFileApi fileApi; private DiskFileApi fileApi;
@Autowired @Autowired
private CustomerMapper customerMapper; private CustomerMapper customerMapper;
@ -62,7 +70,8 @@ public class ApplicationServiceSupport
final Date now = new Date(); final Date now = new Date();
application.setDateUpdated(now); application.setDateUpdated(now);
uploadImages(id, 1, form.getImages(), form.getImageTitles(), form.getCreatedBy()); uploadFiles(id, 1, form.getImages(), form.getImageTitles(), form.getCreatedBy(), ApplicationImage::new, imageMapper::insert);
uploadFiles(id, 1, form.getAttachments(), form.getAttachmentTitles(), form.getCreatedBy(), ApplicationAttachment::new, attachmentMapper::insert);
mapper.insert(application); mapper.insert(application);
return toViewModel(application); return toViewModel(application);
} }
@ -117,11 +126,13 @@ public class ApplicationServiceSupport
*/ */
@Override @Override
public ViewModel<Application> findViewModel(final String id) { public ViewModel<Application> findViewModel(final String id) {
final List<ApplicationImage> images = imageMapper.list( final Search search =
new Search(ApplicationImage.APPLICATION_ID, id) new Search(ApplicationImage.APPLICATION_ID, id)
.asc(ApplicationImage.SORT)); .asc(ApplicationImage.SORT);
return super.findViewModel(id) return super.findViewModel(id)
.setAttr("images", images); .setAttr("images", imageMapper.list(search))
.setAttr("attachments", attachmentMapper.list(search));
} }
/** /**
@ -132,11 +143,21 @@ public class ApplicationServiceSupport
final String applicationId = form.getApplicationId(); final String applicationId = form.getApplicationId();
final String createdBy = form.getCreatedBy(); final String createdBy = form.getCreatedBy();
validatePerm(applicationId, createdBy, form.isAdmin()); validatePerm(applicationId, createdBy, form.isAdmin());
return uploadImages(applicationId, return uploadFiles(applicationId,
imageMapper.nextSort(applicationId), imageMapper.nextSort(applicationId),
new MultipartFile[] {form.getImage()}, new MultipartFile[] {form.getImage()},
new String[] {form.getNote()}, new String[] {form.getNote()},
createdBy).iterator().next(); createdBy,
ApplicationImage::new,
imageMapper::insert).iterator().next();
}
/**
* {@inheritDoc}
*/
@Override
public void updateImage(final ApplicationImageFormForUpdate form) {
updateFile(form, imageMapper::find, imageMapper::listSort, imageMapper::update);
} }
/** /**
@ -145,23 +166,128 @@ public class ApplicationServiceSupport
@Override @Override
@Transactional(rollbackFor = Throwable.class) @Transactional(rollbackFor = Throwable.class)
public void removeImage(final String id, final String userId, final boolean admin) { public void removeImage(final String id, final String userId, final boolean admin) {
final ApplicationImage appImage = imageMapper.find(id); removeFile(id, userId, admin, imageMapper::find, imageMapper::delete, imageMapper::updateSort);
Assert.state(appImage != null, "No application image [" + id + "] found");
final String applicationId = appImage.getApplicationId();
validatePerm(applicationId, userId, admin);
fileApi.delete(imageFolder(applicationId) + "/" + id);
imageMapper.delete(id);
imageMapper.updateSort(applicationId);
} }
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public void updateImage(final ApplicationImageFormForUpdate form) { public ApplicationAttachment addAttachment(final ApplicationAttachmentFormForCreate form) {
final String applicationId = form.getApplicationId();
final String createdBy = form.getCreatedBy();
validatePerm(applicationId, createdBy, form.isAdmin());
return uploadFiles(applicationId,
attachmentMapper.nextSort(applicationId),
new MultipartFile[] {form.getAttachment()},
new String[] {form.getNote()},
createdBy,
ApplicationAttachment::new,
attachmentMapper::insert).iterator().next();
}
/**
* {@inheritDoc}
*/
@Override
public void updateAttachment(final ApplicationAttachmentFormForUpdate form) {
updateFile(form, attachmentMapper::find, attachmentMapper::listSort, attachmentMapper::update);
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Throwable.class)
public void removeAttachment(final String id, final String userId, final boolean admin) {
removeFile(id, userId, admin, attachmentMapper::find, attachmentMapper::delete, attachmentMapper::updateSort);
}
/**
* {@inheritDoc}
*/
@Override
@Transactional(rollbackFor = Throwable.class)
public int delete(final String id) {
log.info("Delete application [{}].", id);
final Application app = find(id);
Assert.state(app != null, "No application [" + id + "] found");
Assert.state(!app.isEnabled(), "Application [" + id + "] is enabled");
Assert.state(customerApplicationMapper.count(
new Search(CustomerApplication.APPLICATION_ID, id)) == 0,
"Application [" + id + "] is in using");
final Search appFileSearch = new Search(ApplicationImage.APPLICATION_ID, id);
imageMapper.list(appFileSearch).forEach(
image -> fileApi.deleteById(image.getFileId()));
imageMapper.delete(appFileSearch);
attachmentMapper.list(appFileSearch).forEach(
attachment -> fileApi.deleteById(attachment.getFileId()));
attachmentMapper.delete(appFileSearch);
return mapper.delete(id);
}
private <T extends ApplicationFile> List<T> uploadFiles(
final String applicationId,
int sort,
final MultipartFile[] files,
final String[] titles,
final String createdBy,
Supplier<T> constructor,
Consumer<T> insert) {
final List<T> appFiles = new ArrayList<>();
final Date now = new Date();
final String fileFolder = fileFolder(applicationId);
int i = 0;
for (final MultipartFile file : files) {
if (!file.isEmpty()) {
final String originalFilename = file.getOriginalFilename();
try (final InputStream ins = file.getInputStream()) {
final String fileId = idSeq.get();
final T appFile = constructor.get();
final FileInfo fileInfo = fileApi.upload(ins,
file.getSize(),
fileFolder,
fileId,
file.getContentType(),
FilenameUtils.getExtension(originalFilename),
originalFilename);
appFile.setId(fileId);
appFile.setApplicationId(applicationId);
appFile.setFileId(fileInfo.getId());
appFile.setUrl(fileInfo.getUrl());
appFile.setSort(sort++);
appFile.setEnabled(true);
appFile.setNote(StringUtils.defaultIfBlank(titles[i], originalFilename));
appFile.setCreatedBy(createdBy);
appFile.setDateCreated(now);
// insert
insert.accept(appFile);
appFiles.add(appFile);
}
catch (final IOException e) {
throw new IllegalStateException(
"Read upload file [" + originalFilename + "] error cased", e);
}
}
else {
log.info("Application file [{}] is empty, ignore.", file);
}
++i;
}
return appFiles;
}
private <T extends ApplicationFile> void updateFile(
final ApplicationFileFormForUpdate form,
final Function<String, T> finder,
final Function<String, List<T>> listSort,
final Consumer<T> updater) {
final String id = form.getId(); final String id = form.getId();
final ApplicationImage appImage = imageMapper.find(id); final T appImage = finder.apply(id);
Assert.state(appImage != null, "No application image [" + id + "] found"); Assert.state(appImage != null, "No application file [" + id + "] found");
final String applicationId = appImage.getApplicationId(); final String applicationId = appImage.getApplicationId();
final String updatedBy = form.getUpdatedBy(); final String updatedBy = form.getUpdatedBy();
validatePerm(applicationId, updatedBy, form.isAdmin()); validatePerm(applicationId, updatedBy, form.isAdmin());
@ -180,75 +306,31 @@ public class ApplicationServiceSupport
appImage.setNote(form.getNote()); appImage.setNote(form.getNote());
appImage.setDateUpdated(new Date()); appImage.setDateUpdated(new Date());
appImage.setUpdatedBy(updatedBy); appImage.setUpdatedBy(updatedBy);
imageMapper.update(appImage);
updater.accept(appImage);
if (sortUpdated) { if (sortUpdated) {
imageMapper.listSort(applicationId).forEach(imageMapper::update); listSort.apply(applicationId).forEach(updater);
} }
} }
/** private <T extends ApplicationFile> void removeFile(
* {@inheritDoc} final String id,
*/ final String userId,
@Override final boolean admin,
@Transactional(rollbackFor = Throwable.class) final Function<String, T> finder,
public int delete(final String id) { final Function<String, Integer> deleter,
log.info("Delete application [{}].", id); final Function<String, Integer> sortUpdater) {
final Application app = find(id); final T appFile = finder.apply(id);
Assert.state(app != null, "No application [" + id + "] found"); Assert.state(appFile != null, "No application file [" + id + "] found");
Assert.state(!app.isEnabled(), "Application [" + id + "] is enabled"); final String applicationId = appFile.getApplicationId();
Assert.state(customerApplicationMapper.count( validatePerm(applicationId, userId, admin);
new Search(CustomerApplication.APPLICATION_ID, id)) == 0, fileApi.delete(fileFolder(applicationId) + "/" + id);
"Application [" + id + "] is in using"); deleter.apply(id);
final Search appImageSearch = new Search(ApplicationImage.APPLICATION_ID, id); sortUpdater.apply(applicationId);
imageMapper.list(appImageSearch).forEach(
image -> fileApi.deleteById(image.getFileId()));
imageMapper.delete(appImageSearch);
return mapper.delete(id);
} }
private List<ApplicationImage> uploadImages( private String fileFolder(final String appId) {
final String applicationId,
int sort,
final MultipartFile[] images,
final String[] titles,
final String createdBy) {
final List<ApplicationImage> imagesRtn = new ArrayList<>();
final Date now = new Date();
final String imageFolder = imageFolder(applicationId);
int i = 0;
for (final MultipartFile image : images) {
if (!image.isEmpty()) {
try (final InputStream ins = image.getInputStream()) {
final String imageId = idSeq.get();
final ApplicationImage appImage = new ApplicationImage();
final FileInfo fileInfo = fileApi.uploadImage(ins, imageFolder, imageId);
appImage.setId(imageId);
appImage.setApplicationId(applicationId);
appImage.setFileId(fileInfo.getId());
appImage.setUrl(fileInfo.getUrl());
appImage.setSort(sort++);
appImage.setEnabled(true);
appImage.setNote(titles[i]);
appImage.setCreatedBy(createdBy);
appImage.setDateCreated(now);
imageMapper.insert(appImage);
imagesRtn.add(appImage);
}
catch (final IOException e) {
throw new IllegalStateException(
"Read upload image [" + image.getOriginalFilename() + "] error cased", e);
}
}
else {
log.info("Application image [{}] is empty, ignore.", image);
}
++i;
}
return imagesRtn;
}
private String imageFolder(final String appId) {
return "app/" + appId; return "app/" + appId;
} }

View File

@ -19,4 +19,5 @@ spring.http.multipart.max-request-size=1024MB
# File # File
file.base-path=http://127.0.0.1:1217/lm-f/ file.base-path=http://127.0.0.1:1217/lm-f/
file.base-dir=/Users/chyxion/Workspaces/ambition-crm/files/ # file.base-dir=/Users/chyxion/Workspaces/ambition-crm/files/
file.base-dir=/Users/donghuang/Documents/Workspaces/ambition-crm/files/

View File

@ -0,0 +1,14 @@
package com.pudonghot.ambition.crm.mapper;
import me.chyxion.tigon.mybatis.BaseMapper;
import com.pudonghot.ambition.crm.model.ApplicationAttachment;
/**
* @author Shaun Chyxion <br>
* chyxion@163.com <br>
* Mar 11, 2018 11:39:49
*/
public interface ApplicationAttachmentMapper
extends ApplicationFileMapper<ApplicationAttachment>,
BaseMapper<String, ApplicationAttachment> {
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* @author Donghuang <br>
* donghuang@wacai.com <br>
* May 17, 2018 17:09:24
*/
-->
<!DOCTYPE mapper PUBLIC
"-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pudonghot.ambition.crm.mapper.ApplicationAttachmentMapper">
<select id="nextSort" resultType="int">
<include refid="com.pudonghot.ambition.crm.mapper.ApplicationFileMapper.nextSort" />
</select>
<update id="updateSort">
<include refid="com.pudonghot.ambition.crm.mapper.ApplicationFileMapper.updateSort" />
</update>
<select id="listSort" resultType="com.pudonghot.ambition.crm.model.ApplicationAttachment">
<include refid="com.pudonghot.ambition.crm.mapper.ApplicationFileMapper.listSort" />
</select>
</mapper>

View File

@ -0,0 +1,38 @@
package com.pudonghot.ambition.crm.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.hibernate.validator.constraints.NotBlank;
import com.pudonghot.ambition.crm.model.ApplicationFile;
/**
* @author Shaun Chyxion <br>
* chyxion@163.com <br>
* Mar 11, 2018 11:39:49
*/
interface ApplicationFileMapper<T extends ApplicationFile> {
/**
* find next sort
* @param applicationId application id
* @return next sort
*/
int nextSort(@NotBlank @Param("applicationId") String applicationId);
/**
* update sort
* @param applicationId application id
* @return effected rows
*/
int updateSort(@NotBlank @Param("applicationId") String applicationId);
/**
* list application files
* @param applicationId
* @return images
*/
List<T> listSort(
@NotBlank
@Param("applicationId")
String applicationId);
}

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* @author Shaun Chyxion <br>
* chyxion@163.com <br>
* May 17, 2018 17:29:10
*/
-->
<!DOCTYPE mapper PUBLIC
"-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pudonghot.ambition.crm.mapper.ApplicationFileMapper">
<sql id="nextSort">
select if (application_id, max(sort) + 1, 1)
from <include refid="table" />
where application_id = #{applicationId}
</sql>
<sql id="updateSort">
update <include refid="table" /> a
join (
select id, @cur_row := @cur_row + 1 sort
from <include refid="table" />
join (select @cur_row := 0) r
where application_id = #{applicationId}
order by <include refid="table" />.sort) s
on a.id = s.id
set a.sort = s.sort
</sql>
<sql id="listSort">
select id, file_id,
url, application_id,
created_by, date_created,
updated_by, date_updated,
enabled, note,
@cur_row := @cur_row + 1 sort
from <include refid="table" />
join (select @cur_row := 0) r
where application_id = #{applicationId}
order by <include refid="table" />.sort
</sql>
</mapper>

View File

@ -1,9 +1,6 @@
package com.pudonghot.ambition.crm.mapper; package com.pudonghot.ambition.crm.mapper;
import java.util.List;
import me.chyxion.tigon.mybatis.BaseMapper; import me.chyxion.tigon.mybatis.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.hibernate.validator.constraints.NotBlank;
import com.pudonghot.ambition.crm.model.ApplicationImage; import com.pudonghot.ambition.crm.model.ApplicationImage;
/** /**
@ -11,29 +8,7 @@ import com.pudonghot.ambition.crm.model.ApplicationImage;
* chyxion@163.com <br> * chyxion@163.com <br>
* Mar 11, 2018 11:39:49 * Mar 11, 2018 11:39:49
*/ */
public interface ApplicationImageMapper extends BaseMapper<String, ApplicationImage> { public interface ApplicationImageMapper
extends ApplicationFileMapper<ApplicationImage>,
/** BaseMapper<String, ApplicationImage> {
* find next sort
* @param applicationId application id
* @return next sort
*/
int nextSort(@NotBlank @Param("applicationId") String applicationId);
/**
* update sort
* @param applicationId application id
* @return effected rows
*/
int updateSort(@NotBlank @Param("applicationId") String applicationId);
/**
* list application images
* @param applicationId
* @return images
*/
List<ApplicationImage> listSort(
@NotBlank
@Param("applicationId")
String applicationId);
} }

View File

@ -12,33 +12,14 @@
<mapper namespace="com.pudonghot.ambition.crm.mapper.ApplicationImageMapper"> <mapper namespace="com.pudonghot.ambition.crm.mapper.ApplicationImageMapper">
<select id="nextSort" resultType="int"> <select id="nextSort" resultType="int">
select if (application_id, max(sort) + 1, 1) <include refid="com.pudonghot.ambition.crm.mapper.ApplicationFileMapper.nextSort" />
from <include refid="table" />
where application_id = #{applicationId}
</select> </select>
<update id="updateSort"> <update id="updateSort">
update crm_application_image a <include refid="com.pudonghot.ambition.crm.mapper.ApplicationFileMapper.updateSort" />
join (
select id, @cur_row := @cur_row + 1 sort
from crm_application_image
join (select @cur_row := 0) r
where application_id = #{applicationId}
order by crm_application_image.sort) s
on a.id = s.id
set a.sort = s.sort
</update> </update>
<select id="listSort" resultType="com.pudonghot.ambition.crm.model.ApplicationImage"> <select id="listSort" resultType="com.pudonghot.ambition.crm.model.ApplicationImage">
select id, file_id, <include refid="com.pudonghot.ambition.crm.mapper.ApplicationFileMapper.listSort" />
url, application_id,
created_by, date_created,
updated_by, date_updated,
enabled, note,
@cur_row := @cur_row + 1 sort
from <include refid="table" />
join (select @cur_row := 0) r
where application_id = #{applicationId}
order by crm_application_image.sort
</select> </select>
</mapper> </mapper>

View File

@ -22,7 +22,18 @@
(select group_concat(note order by sort separator 0x1d) from (select group_concat(note order by sort separator 0x1d) from
crm_application_image crm_application_image
where application_id = a.id where application_id = a.id
group by application_id) image_titles group by application_id) image_titles,
(select group_concat(url order by sort separator 0x1d) from
crm_application_attachment
where application_id = a.id
group by application_id) attachments,
(select group_concat(note order by sort separator 0x1d) from
crm_application_attachment
where application_id = a.id
group by application_id) attachment_titles
from from
<include refid="table" /> a <include refid="table" /> a
<include refid="Tigon.search" /> <include refid="Tigon.search" />

View File

@ -1,10 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" <beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/beans/spring-beans.xsd">
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--<context:component-scan base-package="cn.com.flaginfo.umsapp.ota" /> -->
</beans> </beans>

View File

@ -0,0 +1,27 @@
package com.pudonghot.ambition.crm.form.create;
import lombok.Getter;
import lombok.Setter;
import me.chyxion.tigon.form.FC2;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotBlank;
import org.springframework.web.multipart.MultipartFile;
/**
* @author Donghuang <br>
* donghuang@wacai.com <br>
* May 17, 2018 17:12:56
*/
@Getter
@Setter
public class ApplicationAttachmentFormForCreate extends FC2<String> {
private static final long serialVersionUID = 1L;
// Properties
// current user is admin
private boolean admin;
@NotBlank
private String applicationId;
@NotNull
private MultipartFile attachment;
}

View File

@ -30,4 +30,6 @@ public class ApplicationFormForCreate extends FC2<String> {
private String owner; private String owner;
private String[] imageTitles; private String[] imageTitles;
private MultipartFile[] images; private MultipartFile[] images;
private String[] attachmentTitles;
private MultipartFile[] attachments;
} }

View File

@ -0,0 +1,15 @@
package com.pudonghot.ambition.crm.form.update;
import lombok.Getter;
import lombok.Setter;
/**
* @author Donghuang <br>
* donghuang@wacai.com <br>
* May 17, 2018 17:12:46
*/
@Getter
@Setter
public class ApplicationAttachmentFormForUpdate extends ApplicationFileFormForUpdate {
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,20 @@
package com.pudonghot.ambition.crm.form.update;
import lombok.Getter;
import lombok.Setter;
import me.chyxion.tigon.form.FU2;
/**
* @author Shaun Chyxion <br>
* chyxion@163.com <br>
* May 17, 2018 17:50:22
*/
@Getter
@Setter
public class ApplicationFileFormForUpdate extends FU2<String, String> {
private static final long serialVersionUID = 1L;
// current user is admin
private boolean admin;
private float sort;
}

View File

@ -2,7 +2,6 @@ package com.pudonghot.ambition.crm.form.update;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import me.chyxion.tigon.form.FU2;
/** /**
* @version 0.0.1 * @version 0.0.1
@ -13,10 +12,6 @@ import me.chyxion.tigon.form.FU2;
*/ */
@Getter @Getter
@Setter @Setter
public class ApplicationImageFormForUpdate extends FU2<String, String> { public class ApplicationImageFormForUpdate extends ApplicationFileFormForUpdate {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
// current user is admin
private boolean admin;
private float sort;
} }

View File

@ -32,4 +32,8 @@ public class Application extends M3<String, String> {
private String images; private String images;
@Transient @Transient
private String imageTitles; private String imageTitles;
@Transient
private String attachments;
@Transient
private String attachmentTitles;
} }

View File

@ -0,0 +1,17 @@
package com.pudonghot.ambition.crm.model;
import lombok.Getter;
import lombok.Setter;
import me.chyxion.tigon.mybatis.Table;
/**
* @author Shaun Chyxion <br>
* chyxion@163.com <br>
* May 17, 2018 17:02:51
*/
@Getter
@Setter
@Table("crm_application_attachment")
public class ApplicationAttachment extends ApplicationFile {
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,28 @@
package com.pudonghot.ambition.crm.model;
import lombok.Getter;
import lombok.Setter;
import me.chyxion.tigon.model.M3;
/**
* @author Donghuang <br>
* donghuang@wacai.com <br>
* May 17, 2018 17:23:39
*/
@Getter
@Setter
public class ApplicationFile extends M3<String, String> {
private static final long serialVersionUID = 1L;
// Column Names
public static final String FILE_ID = "file_id";
public static final String URL = "url";
public static final String APPLICATION_ID = "application_id";
public static final String SORT = "sort";
// Properties
private String fileId;
private String url;
private String applicationId;
private float sort;
}

View File

@ -2,7 +2,6 @@ package com.pudonghot.ambition.crm.model;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import me.chyxion.tigon.model.M3;
import me.chyxion.tigon.mybatis.Table; import me.chyxion.tigon.mybatis.Table;
/** /**
@ -13,18 +12,6 @@ import me.chyxion.tigon.mybatis.Table;
@Getter @Getter
@Setter @Setter
@Table("crm_application_image") @Table("crm_application_image")
public class ApplicationImage extends M3<String, String> { public class ApplicationImage extends ApplicationFile {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
// Column Names
public static final String FILE_ID = "file_id";
public static final String URL = "url";
public static final String APPLICATION_ID = "application_id";
public static final String SORT = "sort";
// Properties
private String fileId;
private String url;
private String applicationId;
private float sort;
} }

View File

@ -0,0 +1,51 @@
import Ember from 'ember';
import Component from '@ember/component';
import { computed } from '@ember/object';
export default Component.extend({
file: {},
value: computed.alias('file.file'),
'no-file': 'Choose file',
'btn-choose': 'Choose file',
didReceiveAttrs() {
const me = this;
me._super(...arguments);
},
didInsertElement() {
let me = this;
me._super(...arguments);
me.$('input[type=file]').ace_file_input({
no_file: me.get('no-file'),
btn_choose: me.get('btn-choose'),
btn_change: null,
droppable: true,
style: 'well',
no_icon: 'ace-icon fa fa-files-o',
// thumbnail: 'large',
// allowExt: ['jpg', 'jpeg', 'png', 'gif'],
// allowMime: ['image/jpg', 'image/jpeg', 'image/png', 'image/gif'],
before_change: function() {
let filename = me.$(this).val();
me.set('value', filename);
return true;
},
before_remove: function() {
let filename = me.$(this).val();
me.set('value', null);
return true;
}
}).on('file.error.ace', function(e, info) {
if (me.$(this)[0] === e.target) {
if (info.error_count['ext'] > 0 || info.error_count['mime'] > 0) {
me.get('message').warn('Invalid file');
}
else if(info.error_count['size'] > 0) {
me.get('message').warn('File size too large');
}
}
});
}
});

View File

@ -15,17 +15,22 @@ export default BaseRoute.extend({
return RSVP.hash({ return RSVP.hash({
enabled: true, enabled: true,
images: [{}], images: [{}],
attachments: [{}],
users: this.get('store').ajaxGet('user/list-for-select') users: this.get('store').ajaxGet('user/list-for-select')
}); });
}, },
actions: { actions: {
addImage() { addImage() {
const me = this; this.get('controller.model.images').pushObject({});
me.get('controller.model.images').pushObject({});
}, },
removeImage(image) { removeImage(image) {
const me = this; this.get('controller.model.images').removeObject(image);
me.get('controller.model.images').removeObject(image);
}, },
addAttachment() {
this.get('controller.model.attachments').pushObject({});
},
removeAttachment(attachment) {
this.get('controller.model.attachments').removeObject(attachment);
}
} }
}); });

View File

@ -23,6 +23,7 @@ export default BaseEditRoute.extend({
}, },
submitAddImage() { submitAddImage() {
const me = this; const me = this;
me.set('controller.errors', {});
if (me.get('controller.model.image.file')) { if (me.get('controller.model.image.file')) {
me.set('controller.errors', {}); me.set('controller.errors', {});
me.get('store').ajaxPost('application/add-image', me.get('store').ajaxPost('application/add-image',
@ -49,30 +50,79 @@ export default BaseEditRoute.extend({
}); });
}); });
}, },
moveUp(image) { moveImageUp(image) {
let me = this; const me = this;
let images = me.get('controller.model.images'); me.moveUp(me.get('controller.model.images'), image, 'updateImage');
if (images && images.length > 1) { },
let index = images.indexOf(image); moveImageDown(image) {
images.removeObject(image); const me = this;
images.insertAt(index - 1, image); me.moveDown(me.get('controller.model.images'), image, 'updateImage');
--image.sort; },
me.updateImage(image); // attachments
addAttachment() {
const me = this;
me.set('controller.model.addAttachment', true);
me.set('controller.model.attachment', {});
},
submitAddAttachment() {
const me = this;
me.set('controller.errors', {});
if (me.get('controller.model.attachment.file')) {
me.get('store').ajaxPost('application/add-attachment',
new FormData($('#form_' + me.get('controller.model.id'))[0]))
.then(attachment => {
me.get('controller.model.attachments').pushObject(attachment);
me.set('controller.model.addAttachment', false);
});
}
else {
me.set('controller.errors.attachment', ['No attachment file selected']);
} }
}, },
moveDown(image) { closeAddAttachment() {
let me = this; const me = this;
let images = me.get('controller.model.images'); me.set('controller.model.addAttachment', false);
if (images && images.length > 1) { },
let index = images.indexOf(image); removeAttachment(attachment) {
images.removeObject(image); const me = this;
images.insertAt(index + 1, image); me.get('dialog').confirm('Are you sure to remove attachment?', () => {
++image.sort; me.get('store').ajaxPost('application/remove-attachment', attachment).then(() => {
me.updateImage(image); me.get('message').alert('Attachment removed');
} me.get('controller.model.attachments').removeObject(attachment);
});
});
},
moveAttachmentUp(attachment) {
const me = this;
me.moveUp(me.get('controller.model.attachments'), attachment, 'updateAttachment');
},
moveAttachmentDown(attachment) {
const me = this;
me.moveDown(me.get('controller.model.attachments'), attachment, 'updateAttachment');
} }
}, },
updateImage(image) { updateImage(image) {
this.get('ajax').doPost('application/update-image', image, false); this.get('ajax').doPost('application/update-image', image, false);
},
updateAttachment(attachment) {
this.get('ajax').doPost('application/update-attachment', attachment, false);
},
moveUp(files, file, update) {
if (files && files.length > 1) {
let index = files.indexOf(file);
files.removeObject(file);
files.insertAt(index - 1, file);
--file.sort;
this[update](file);
}
},
moveDown(files, file, update) {
if (files && files.length > 1) {
let index = files.indexOf(file);
files.removeObject(file);
files.insertAt(index + 1, file);
++file.sort;
this[update](file);
}
} }
}); });

View File

@ -38,6 +38,22 @@
<div class="space-12"></div> <div class="space-12"></div>
{{/each}} {{/each}}
{{/with}} {{/with}}
<hr />
{{#with (customer-application-images model.attachments model.attachmentTitles) as |attachments|}}
{{#if attachments.length}}
<div class="row">
{{#each attachments as |attachment index|}}
<div class="col-xs-12">
<a href="{{attachment.image}}" title="{{attachment.title}}" target="_blank">
<i class="ace-icon fa fa-paperclip bigger-120 grey"></i>
{{if attachment.title attachment.title 'Attachment'}}
</a>
<div class="space-2"></div>
</div>
{{/each}}
</div>
{{/if}}
{{/with}}
</div> </div>
{{/modal-dialog}} {{/modal-dialog}}
{{/if}} {{/if}}

View File

@ -0,0 +1 @@
{{input type='file' name=name}}

View File

@ -1,5 +1,5 @@
<label for="{{if name name idField}}" class="{{get this 'label-class'}} control-label no-padding-right"> {{label}} </label> <label for="{{if name name idField}}" class="{{label-class}} control-label no-padding-right"> {{label}} </label>
<div class="{{get this 'input-class'}}"> <div class="{{input-class}}">
{{#if hasBlock}} {{#if hasBlock}}
{{yield}} {{yield}}

View File

@ -40,6 +40,34 @@
</div> </div>
{{/form-input}} {{/form-input}}
{{#form-input name='attachment' label='Attachments'}}
<div class="widget-box transparent">
<div class="widget-header widget-header-small">
<div class="widget-toolbar action-buttons">
<a href="#" data-rel="tooltip" title="Add Attachment" {{action (route-action 'addAttachment')}}>
<i class="ace-icon fa fa-plus-circle green"></i>
</a>
</div>
</div>
<div class="widget-body">
<div class="widget-main padding-4">
{{#each model.attachments as |attachment i|}}
<div class="col-xs-6 no-padding-left">
<div class="space-4"></div>
{{input name=(concat 'attachmentTitles[' i ']') class='width-80' placeholder='Attachment name' value=image.title}}
&nbsp;
<a href="#" class="red" data-rel="tooltip" title="Remove Attachment" {{action (route-action 'removeAttachment' attachment)}}>
<i class="ace-icon fa fa-times bigger-125"></i>
</a>
<div class="space-2"></div>
{{!input type='file'}}
{{file-input name=(concat 'attachments[' i ']') file=attachment}}
</div>
{{/each}}
</div>
</div>
</div>
{{/form-input}}
<hr /> <hr />
{{form-footer-buttons}} {{form-footer-buttons}}
{{/form-content}} {{/form-content}}

View File

@ -71,12 +71,83 @@
<i class="ace-icon fa fa-trash-o"></i> <i class="ace-icon fa fa-trash-o"></i>
</a> </a>
{{#if (not-eq model.images.firstObject.id image.id)}} {{#if (not-eq model.images.firstObject.id image.id)}}
<a class="btn btn-xs btn-info" data-rel="tooltip" title="Move Up" {{action (route-action 'moveUp' image)}}> <a class="btn btn-xs btn-info" data-rel="tooltip" title="Move Up" {{action (route-action 'moveImageUp' image)}}>
<i class="ace-icon fa fa-arrow-up bigger-90"></i> <i class="ace-icon fa fa-arrow-up bigger-90"></i>
</a> </a>
{{/if}} {{/if}}
{{#if (not-eq model.images.lastObject.id image.id)}} {{#if (not-eq model.images.lastObject.id image.id)}}
<a class="btn btn-xs btn-success" data-rel="tooltip" title="Move Down" {{action (route-action 'moveDown' image)}}> <a class="btn btn-xs btn-success" data-rel="tooltip" title="Move Down" {{action (route-action 'moveImageDown' image)}}>
<i class="ace-icon fa fa-arrow-down bigger-90"></i>
</a>
{{/if}}
</div>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
{{/form-input}}
{{#form-input label='Attachments' name='attachments'}}
<div class="widget-box transparent" style="opacity: 1;">
<!-- #section:custom/widget-box.options -->
<div class="widget-header">
<div class="widget-toolbar action-buttons">
<a href="#" data-rel="tooltip" title="Add Attachment" {{action (route-action 'addAttachment')}}>
<i class="ace-icon fa fa-plus-circle green"></i>
</a>
</div>
</div>
<!-- /section:custom/widget-box.options -->
<div class="widget-body">
<div class="widget-main no-padding">
<table class="table table-striped table-bordered table-hover">
<thead class="thin-border-bottom">
<tr>
<th style="width: 86px;">
<i class="ace-icon fa fa-paperclip bigger-110 hidden-480"></i>
Attachment
</th>
<th>
<i class="ace-icon fa fa-sticky-note-o bigger-110 hidden-480"></i>
Description
</th>
<th style="width: 106px;">
<i class="ace-icon fa fa-cogs bigger-110 hidden-480"></i>
Settings
</th>
</tr>
</thead>
<tbody>
{{#each model.attachments as |attachment|}}
<tr>
<td>
{{!image-previews images=image.url}}
<a href="{{attachment.url}}" target="_blank">
<i class="ace-icon fa fa-download bigger-110 grey"></i>
Download
</a>
</td>
<td>
{{editable-cell model=attachment field='note' post-url='application/update-attachment'}}
</td>
<td>
<div class="btn-group">
<a class="btn btn-xs btn-danger" data-rel="tooltip" title="Remove" {{action (route-action 'removeAttachment' attachment)}}>
<i class="ace-icon fa fa-trash-o"></i>
</a>
{{#if (not-eq model.images.firstObject.id attachment.id)}}
<a class="btn btn-xs btn-info" data-rel="tooltip" title="Move Up" {{action (route-action 'moveAttachmentUp' attachment)}}>
<i class="ace-icon fa fa-arrow-up bigger-90"></i>
</a>
{{/if}}
{{#if (not-eq model.images.lastObject.id attachment.id)}}
<a class="btn btn-xs btn-success" data-rel="tooltip" title="Move Down" {{action (route-action 'moveAttachmentDown' attachment)}}>
<i class="ace-icon fa fa-arrow-down bigger-90"></i> <i class="ace-icon fa fa-arrow-down bigger-90"></i>
</a> </a>
{{/if}} {{/if}}
@ -109,3 +180,17 @@
{{/form-content}} {{/form-content}}
{{/modal-dialog}} {{/modal-dialog}}
{{/if}} {{/if}}
{{#if model.addAttachment}}
{{#modal-dialog title='Add Attachment' no-cancel=true submit=(route-action 'submitAddAttachment') on-close=(route-action 'closeAddAttachment') close-to-parent=false}}
{{#form-content form-id=(concat 'form_' model.id)}}
{{input type='hidden' name='applicationId' value=model.id}}
{{#form-input name='note' label='Description'}}
{{input name='note' value=model.attachment.note class='col-xs-12'}}
{{/form-input}}
{{#form-input name='attachment' label='Attachment'}}
{{file-input name='attachment' file=model.attachment}}
{{/form-input}}
{{/form-content}}
{{/modal-dialog}}
{{/if}}

View File

@ -0,0 +1,24 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
moduleForComponent('file-input', 'Integration | Component | file input', {
integration: true
});
test('it renders', function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
this.render(hbs`{{file-input}}`);
assert.equal(this.$().text().trim(), '');
// Template block usage:
this.render(hbs`
{{#file-input}}
template block text
{{/file-input}}
`);
assert.equal(this.$().text().trim(), 'template block text');
});