RB-525 Announcement in feeds (#105)

* feat: announcement
* TestCase pass
This commit is contained in:
devezhao 2019-12-19 22:47:25 +08:00 committed by GitHub
parent e870b0adf4
commit 1bbf977ac0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 528 additions and 89 deletions

View file

@ -14,7 +14,9 @@
"editor.fontSize": 12,
"editor.tabSize": 2,
"editor.formatOnSave": true,
"eslint.autoFixOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.options": {
"configFile": "./.eslintrc.json"
},
@ -26,6 +28,4 @@
"prettier.eslintIntegration": true,
"workbench.editor.enablePreview": false,
"java.configuration.updateBuildConfiguration": "disabled"
}
// node and eslint(-g)
// Plugins: Beautify and ESLint
}

View file

@ -92,7 +92,7 @@ public class LoginToken extends BaseApi {
* @return
*/
public static String checkUser(String user, String password) {
if (!Application.getUserStore().exists(user)) {
if (!Application.getUserStore().existsUser(user)) {
return Languages.lang("InputWrong", "UsernameOrPassword");
}

View file

@ -24,12 +24,14 @@ import cn.devezhao.persist4j.engine.ID;
import com.rebuild.server.Application;
import com.rebuild.server.metadata.EntityHelper;
import com.rebuild.server.service.bizz.UserHelper;
import com.rebuild.server.service.notification.MessageBuilder;
import org.apache.commons.lang.StringUtils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
/**
* @author devezhao
@ -163,4 +165,23 @@ public class FeedsHelper {
}
return false;
}
/**
* 格式化动态内容
*
* @param content
* @return
*/
public static String formatContent(String content) {
Matcher atMatcher = MessageBuilder.AT_PATTERN.matcher(content);
while (atMatcher.find()) {
String at = atMatcher.group();
ID user = ID.valueOf(at.substring(1));
if (user.getEntityCode() == EntityHelper.User && Application.getUserStore().existsUser(user)) {
String fullName = Application.getUserStore().getUser(user).getFullName();
content = content.replace(at, String.format("<a data-id=\"%s\">@%s</a>", user, fullName));
}
}
return content;
}
}

View file

@ -28,6 +28,7 @@ public enum FeedsType {
ACTIVITY(1, "动态"),
FOLLOWUP(2, "跟进"),
ANNOUNCEMENT(3, "公告"),
;

View file

@ -25,6 +25,7 @@ import com.rebuild.utils.JSONable;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -77,6 +78,7 @@ public class LanguageBundle implements JSONable {
/**
* @return
* @see Locale#forLanguageTag(String)
*/
public String locale() {
return locale;

View file

@ -111,7 +111,7 @@ public class UserService extends SystemEntityService {
record.setString("password", EncryptUtils.toSHA256Hex(password));
}
if (record.hasValue("email") && Application.getUserStore().exists(record.getString("email"))) {
if (record.hasValue("email") && Application.getUserStore().existsUser(record.getString("email"))) {
throw new DataSpecificationException(Languages.lang("Repeated", "Email"));
}
@ -133,7 +133,7 @@ public class UserService extends SystemEntityService {
* @throws DataSpecificationException
*/
private void checkLoginName(String loginName) throws DataSpecificationException {
if (Application.getUserStore().exists(loginName)) {
if (Application.getUserStore().existsUser(loginName)) {
throw new DataSpecificationException("登陆名重复");
}
if (!CommonsUtils.isPlainText(loginName) || BlackList.isBlack(loginName)) {

View file

@ -31,6 +31,7 @@ import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.server.Application;
import com.rebuild.server.metadata.EntityHelper;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@ -89,17 +90,34 @@ public class UserStore {
* @param emailOrName
* @return
*/
public boolean exists(String emailOrName) {
public boolean existsUser(String emailOrName) {
return existsName(emailOrName) || existsEmail(emailOrName);
}
/**
* @param userId
* @return
*/
public boolean exists(ID userId) {
public boolean existsUser(ID userId) {
return USERs.containsKey(userId);
}
/**
* @param bizzId
* @return
*/
public boolean existsAny(ID bizzId) {
if (bizzId.getEntityCode() == EntityHelper.User) {
return USERs.containsKey(bizzId);
} else if (bizzId.getEntityCode() == EntityHelper.Role) {
return ROLEs.containsKey(bizzId);
} else if (bizzId.getEntityCode() == EntityHelper.Department) {
return DEPTs.containsKey(bizzId);
} else if (bizzId.getEntityCode() == EntityHelper.Team) {
return TEAMs.containsKey(bizzId);
}
return false;
}
/**
* @param username
@ -249,7 +267,7 @@ public class UserStore {
final ID deptId = (ID) o[6];
final ID roleId = (ID) o[7];
final User oldUser = exists(userId) ? getUser(userId) : null;
final User oldUser = existsUser(userId) ? getUser(userId) : null;
if (oldUser != null) {
Role role = oldUser.getOwningRole();
if (role != null) {

View file

@ -139,7 +139,7 @@ public class MessageBuilder {
final ID id = ID.valueOf(atid);
if (id.getEntityCode() == EntityHelper.User) {
if (Application.getUserStore().exists(id)) {
if (Application.getUserStore().existsUser(id)) {
return Application.getUserStore().getUser(id).getFullName();
} else {
return "[无效用户]";

View file

@ -210,7 +210,8 @@ public class RequestWatchHandler extends HandlerInterceptorAdapter {
reqUrl = reqUrl.replaceFirst(ServerListener.getContextPath(), "");
return reqUrl.startsWith("/gw/") || reqUrl.startsWith("/assets/") || reqUrl.startsWith("/error/")
|| reqUrl.startsWith("/t/") || reqUrl.startsWith("/s/")
|| reqUrl.startsWith("/setup/") || reqUrl.startsWith("/language/");
|| reqUrl.startsWith("/setup/") || reqUrl.startsWith("/language/")
|| reqUrl.startsWith("/commons/announcements");
}
/**

View file

@ -67,7 +67,7 @@ public class UserControll extends BaseEntityControll {
@RequestMapping("check-user-status")
public void checkUserStatus(HttpServletRequest request, HttpServletResponse response) throws IOException {
ID id = getIdParameterNotNull(request, "id");
if (!Application.getUserStore().exists(id)) {
if (!Application.getUserStore().existsUser(id)) {
writeFailure(response);
return;
}

View file

@ -0,0 +1,118 @@
/*
rebuild - Building your business-systems freely.
Copyright (C) 2018-2019 devezhao <zhaofang123@gmail.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rebuild.web.feeds;
import cn.devezhao.commons.CalendarUtils;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.server.Application;
import com.rebuild.server.business.feeds.FeedsHelper;
import com.rebuild.server.business.feeds.FeedsScope;
import com.rebuild.server.service.bizz.UserHelper;
import com.rebuild.utils.AppUtils;
import com.rebuild.web.BaseControll;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 动态公告
*
* @author devezhao
* @since 2019/12/19
*/
@Controller
public class AnnouncementControll extends BaseControll {
@RequestMapping("/commons/announcements")
public void list(HttpServletRequest request, HttpServletResponse response) throws IOException {
ID user = AppUtils.getRequestUser(request);
int fromWhere = 0;
// 1=动态页 2=首页 4=登录页
String refererUrl = request.getHeader("Referer");
if (refererUrl.contains("/user/login")) {
fromWhere = 4;
} else if (refererUrl.contains("/dashboard/home")) {
fromWhere = 2;
} else if (refererUrl.contains("/feeds/")) {
fromWhere = 1;
}
Object[][] array = Application.createQueryNoFilter(
"select content,contentMore,scope,createdBy,createdOn,feedsId from Feeds where type = 3")
.array();
List<JSON> as = new ArrayList<>();
long timeNow = CalendarUtils.now().getTime();
for (Object[] o : array) {
JSONObject options = JSON.parseObject((String) o[1]);
// 不在指定位置
int whereMask = options.getIntValue("showWhere");
if ((fromWhere & whereMask) == 0) {
continue;
}
// 不在公示时间
Date timeStart = StringUtils.isBlank(options.getString("timeStart")) ? null : CalendarUtils.parse(options.getString("timeStart"));
if (timeStart != null && timeNow < timeStart.getTime()) {
continue;
}
Date timeEnd = StringUtils.isBlank(options.getString("timeEnd")) ? null : CalendarUtils.parse(options.getString("timeEnd"));
if (timeEnd != null && timeNow > timeEnd.getTime()) {
continue;
}
// 不可见
boolean allow = false;
String scope = (String) o[2];
if (FeedsScope.ALL.name().equalsIgnoreCase(scope)) {
allow = true;
} else if (FeedsScope.SELF.name().equalsIgnoreCase(scope) && o[3].equals(user)) {
allow = true;
} else if (ID.isId(scope) && user != null) {
ID teamId = ID.valueOf(scope);
if (Application.getUserStore().existsAny(teamId)) {
allow = Application.getUserStore().getTeam(teamId).isMember(user);
}
}
if (allow) {
JSONObject a = new JSONObject();
a.put("content", FeedsHelper.formatContent((String) o[0]));
a.put("publishOn", CalendarUtils.getUTCDateTimeFormat().format(o[4]));
a.put("publishBy", UserHelper.getName((ID) o[3]));
a.put("id", o[5]);
as.add(a);
}
}
writeSuccess(response, as);
}
}

View file

@ -29,11 +29,9 @@ import com.rebuild.server.Application;
import com.rebuild.server.business.feeds.FeedsHelper;
import com.rebuild.server.business.feeds.FeedsScope;
import com.rebuild.server.configuration.portals.FieldValueWrapper;
import com.rebuild.server.metadata.EntityHelper;
import com.rebuild.server.metadata.MetadataHelper;
import com.rebuild.server.metadata.entity.EasyMeta;
import com.rebuild.server.service.bizz.UserHelper;
import com.rebuild.server.service.notification.MessageBuilder;
import com.rebuild.server.service.query.AdvFilterParser;
import com.rebuild.utils.JSONUtils;
import com.rebuild.web.BasePageControll;
@ -50,7 +48,6 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
/**
* 列表相关
@ -120,7 +117,7 @@ public class FeedsListControll extends BasePageControll {
}
}
String sql = "select feedsId,createdBy,createdOn,modifiedOn,content,images,attachments,scope,type,relatedRecord from Feeds where " + sqlWhere;
String sql = "select feedsId,createdBy,createdOn,modifiedOn,content,images,attachments,scope,type,relatedRecord,contentMore from Feeds where " + sqlWhere;
if ("older".equalsIgnoreCase(sort)) {
sql += " order by createdOn asc";
} else if ("modified".equalsIgnoreCase(sort)) {
@ -157,6 +154,11 @@ public class FeedsListControll extends BasePageControll {
item.put("related", mixValue);
}
// 更多内容
if (o[10] != null) {
item.put("contentMore", JSON.parse((String) o[10]));
}
list.add(item);
}
writeSuccess(response,
@ -211,7 +213,7 @@ public class FeedsListControll extends BasePageControll {
item.put("createdOn", CalendarUtils.getUTCDateTimeFormat().format(o[2]));
item.put("createdOnFN", Moment.moment((Date) o[2]).fromNow());
item.put("modifedOn", CalendarUtils.getUTCDateTimeFormat().format(o[3]));
item.put("content", formatContent((String) o[4]));
item.put("content", FeedsHelper.formatContent((String) o[4]));
if (o[5] != null) {
item.put("images", JSON.parse((String) o[5]));
}
@ -226,21 +228,4 @@ public class FeedsListControll extends BasePageControll {
}
return item;
}
/**
* @param content
* @return
*/
private String formatContent(String content) {
Matcher atMatcher = MessageBuilder.AT_PATTERN.matcher(content);
while (atMatcher.find()) {
String at = atMatcher.group();
ID user = ID.valueOf(at.substring(1));
if (user.getEntityCode() == EntityHelper.User && Application.getUserStore().exists(user)) {
String fullName = Application.getUserStore().getUser(user).getFullName();
content = content.replace(at, String.format("<a data-id=\"%s\">@%s</a>", user, fullName));
}
}
return content;
}
}

View file

@ -119,7 +119,7 @@ public class LoginControll extends BasePageControll {
LOG.error("Can't decode User from alt : " + alt, ex);
}
if (altUser != null && Application.getUserStore().exists(altUser)) {
if (altUser != null && Application.getUserStore().existsUser(altUser)) {
loginSuccessed(request, response, altUser, true);
String nexturl = StringUtils.defaultIfBlank(request.getParameter("nexturl"), DEFAULT_HOME);

View file

@ -338,6 +338,7 @@
<field name="feedsId" type="primary" />
<field name="type" type="small-int" nullable="false" updatable="false" default-value="1" description="类型" />
<field name="content" type="text" nullable="false" max-length="3000" description="内容" />
<field name="contentMore" type="text" max-length="3000" description="不同类型的扩展内容, JSON格式KV" />
<field name="images" type="string" max-length="700" description="图片" extra-attrs="{displayType:'IMAGE'}" />
<field name="attachments" type="string" max-length="700" description="附件" extra-attrs="{displayType:'FILE'}" />
<field name="relatedRecord" type="any-reference" description="相关业务记录" />

View file

@ -498,6 +498,7 @@ create table if not exists `feeds` (
`FEEDS_ID` char(20) not null,
`TYPE` smallint(6) not null default '1' comment '类型',
`CONTENT` text(3000) not null comment '内容',
`CONTENT_MORE` text(3000) comment '不同类型的扩展内容, JSON格式KV',
`IMAGES` varchar(700) comment '图片',
`ATTACHMENTS` varchar(700) comment '附件',
`RELATED_RECORD` char(20) comment '相关业务记录',
@ -588,4 +589,4 @@ INSERT INTO `classification` (`DATA_ID`, `NAME`, `DESCRIPTION`, `OPEN_LEVEL`, `I
-- DB Version
INSERT INTO `system_config` (`CONFIG_ID`, `ITEM`, `VALUE`)
VALUES ('021-9000000000000001', 'DBVer', 18);
VALUES ('021-9000000000000001', 'DBVer', 19);

View file

@ -1,6 +1,10 @@
-- Database upgrade scripts for rebuild 1.x
-- Each upgraded starts with `-- #VERSION`
-- #19 Announcement
alter table `feeds`
add column `CONTENT_MORE` text(3000) comment '不同类型的扩展内容, JSON格式KV';
-- #18 Folder scope
alter table `attachment_folder`
add column `SCOPE` varchar(20) default 'ALL' comment '哪些人可见, 可选值: ALL/SELF/$TeamID',

View file

@ -206,6 +206,7 @@
.rich-content>.related {
margin-bottom: 6px;
font-size: 12px;
}
.rich-content>.img-field {
@ -459,9 +460,9 @@
}
.related-select {
background-color: #f5f5f5;
border-radius: 2px;
margin-top: 10px;
background-color: #eee;
}
.fixed-icon {
@ -493,6 +494,7 @@
border-top: 1px solid #4285f4;
width: 32px;
transition: margin-left linear 0.1s;
margin-left: 8px;
}
.arrow_box:after,
@ -519,4 +521,36 @@
border-bottom-color: #4285f4;
border-width: 7px;
margin-left: -7px;
}
.announcement-options {
margin-top: 10px;
border-radius: 2px;
padding-top: 18px;
border: 1px solid #eee;
}
.announcement-options dt {
color: #777;
font-weight: normal;
text-align: right;
padding-top: 6px;
}
.announcement-options .input-group {
max-width: 430px;
}
.announcement-options .input-group-prepend {
height: 37px;
}
.rich-content>.mores {
font-size: 12px;
margin-bottom: 6px;
line-height: 1.6;
}
.rich-content>.mores span {
color: #666;
}

View file

@ -21471,7 +21471,9 @@ fieldset[disabled] .btn-color.btn-evernote:hover {
.alert .message {
display: table-cell;
padding: 1.385rem 2.1542rem 1.385rem .231rem;
border-left-width: 0
border-left-width: 0;
word-break: break-all;
word-wrap: break-word;
}
@media (max-width:575.98px) {

View file

@ -3090,4 +3090,50 @@ a.icon-link>.zmdi {
color: #999;
font-weight: normal;
font-size: 12px;
}
.announcement-wrapper>div {
margin: 0;
border-radius: 0;
color: #fff;
padding: 15px 25px;
font-size: 1rem;
}
.announcement-wrapper>div:hover {
cursor: pointer;
color: #eee;
}
.announcement-wrapper>div+div {
margin-top: 1px;
}
.announcement-wrapper>div .icon {
float: left;
font-size: 1.65rem;
}
.announcement-wrapper>div p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
padding-left: 15px;
margin: 0;
}
.announcement-contents {
line-height: 1.7;
font-size: 1rem;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-bottom: 10px;
}
.announcement-contents img.emoji {
width: 26px;
height: 26px;
line-height: 1;
font-size: 0;
}

View file

@ -228,15 +228,14 @@ class DlgDashSettings extends RbFormHandler {
<input className="form-control form-control-sm" value={this.state.title || ''} placeholder="默认仪表盘" data-id="title" onChange={this.handleChange} maxLength="40" />
</div>
</div>
{rb.isAdminUser !== true ? null :
<div className="form-group row">
<label className="col-sm-3 col-form-label text-sm-right"></label>
<div className="col-sm-7">
<div className="shareTo--wrap">
<Share2 ref={(c) => this._shareTo = c} noSwitch={true} shareTo={this.props.shareTo} />
</div>
{rb.isAdminUser && <div className="form-group row">
<label className="col-sm-3 col-form-label text-sm-right"></label>
<div className="col-sm-7">
<div className="shareTo--wrap">
<Share2 ref={(c) => this._shareTo = c} noSwitch={true} shareTo={this.props.shareTo} />
</div>
</div>
</div>
}
<div className="form-group row footer">
<div className="col-sm-7 offset-sm-3">

View file

@ -0,0 +1,65 @@
/* eslint-disable react/prop-types */
const EMOJIS = { '赞': 'fs_zan.png', '握手': 'fs_woshou.png', '耶': 'fs_ye.png', '抱拳': 'fs_baoquan.png', 'OK': 'fs_ok.png', '拍手': 'fs_paishou.png', '拜托': 'fs_baituo.png', '差评': 'fs_chaping.png', '微笑': 'fs_weixiao.png', '撇嘴': 'fs_piezui.png', '花痴': 'fs_huachi.png', '发呆': 'fs_fadai.png', '得意': 'fs_deyi.png', '大哭': 'fs_daku.png', '害羞': 'fs_haixiu.png', '闭嘴': 'fs_bizui.png', '睡着': 'fs_shuizhao.png', '敬礼': 'fs_jingli.png', '崇拜': 'fs_chongbai.png', '抱抱': 'fs_baobao.png', '忍住不哭': 'fs_renzhubuku.png', '尴尬': 'fs_ganga.png', '发怒': 'fs_fanu.png', '调皮': 'fs_tiaopi.png', '开心': 'fs_kaixin.png', '惊讶': 'fs_jingya.png', '呵呵': 'fs_hehe.png', '思考': 'fs_sikao.png', '哭笑不得': 'fs_kuxiaobude.png', '抓狂': 'fs_zhuakuang.png', '呕吐': 'fs_outu.png', '偷笑': 'fs_touxiao.png', '笑哭了': 'fs_xiaokule.png', '白眼': 'fs_baiyan.png', '傲慢': 'fs_aoman.png', '饥饿': 'fs_jie.png', '困': 'fs_kun.png', '吓': 'fs_xia.png', '流汗': 'fs_liuhan.png', '憨笑': 'fs_hanxiao.png', '悠闲': 'fs_youxian.png', '奋斗': 'fs_fendou.png', '咒骂': 'fs_zhouma.png', '疑问': 'fs_yiwen.png', '嘘': 'fs_xu.png', '晕': 'fs_yun.png', '惊恐': 'fs_jingkong.png', '衰': 'fs_shuai.png', '骷髅': 'fs_kulou.png', '敲打': 'fs_qiaoda.png', '再见': 'fs_zaijian.png', '无语': 'fs_wuyu.png', '抠鼻': 'fs_koubi.png', '鼓掌': 'fs_guzhang.png', '糗大了': 'fs_qiudale.png', '猥琐的笑': 'fs_weisuodexiao.png', '哼': 'fs_heng.png', '不爽': 'fs_bushuang.png', '打哈欠': 'fs_dahaqian.png', '鄙视': 'fs_bishi.png', '委屈': 'fs_weiqu.png', '安慰': 'fs_anwei.png', '坏笑': 'fs_huaixiao.png', '亲亲': 'fs_qinqin.png', '冷汗': 'fs_lenghan.png', '可怜': 'fs_kelian.png', '生病': 'fs_shengbing.png', '愉快': 'fs_yukuai.png', '幸灾乐祸': 'fs_xingzailehuo.png', '大便': 'fs_dabian.png', '干杯': 'fs_ganbei.png', '钱': 'fs_qian.png' }
// eslint-disable-next-line no-unused-vars
const converEmoji = function (text) {
let es = text.match(/\[(.+?)\]/g)
if (!es) return text
es.forEach((e) => {
let img = EMOJIS[e.substr(1, e.length - 2)]
if (img) {
img = `<img class="emoji" src="${rb.baseUrl}/assets/img/emoji/${img}"/>`
text = text.replace(e, img)
}
})
return text.replace(/\n/g, '<br />')
}
//
class AnnouncementModal extends React.Component {
state = { ...this.props }
render() {
const contentHtml = converEmoji(this.props.content.replace(/\n/g, '<br>'))
return <div className="modal" tabIndex={this.state.tabIndex || -1} ref={(c) => this._dlg = c}>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header pb-0">
<button className="close" type="button" onClick={this.hide}><i className="zmdi zmdi-close" /></button>
</div>
<div className="modal-body">
<div className="text-break announcement-contents" dangerouslySetInnerHTML={{ __html: contentHtml }} />
<div>
<span className="float-left text-muted fs-12"> {this.props.publishBy} 发布于 {this.props.publishOn}</span>
<span className="float-right"><a href={`${rb.baseUrl}/app/list-and-view?id=${this.props.id}`}>前往动态查看</a></span>
<span className="clearfi"></span>
</div>
</div>
</div>
</div>
</div >
}
componentDidMount() {
let root = $(this._dlg).modal({ show: true, keyboard: true }).on('hidden.bs.modal', () => {
root.modal('dispose')
$unmount(root.parent())
})
}
hide = () => $(this._dlg).modal('hide')
}
var $showAnnouncement = function () {
$.get(`${rb.baseUrl}/commons/announcements`, (res) => {
if (res.error_code !== 0 || !res.data || res.data.length === 0) return
let as = res.data.map((item, idx) => {
return <div className="bg-primary" key={'a-' + idx} title="查看详情"
onClick={() => renderRbcomp(<AnnouncementModal {...item} />)}>
<i className="icon zmdi zmdi-notifications-active" />
<p>{item.content}</p>
</div>
})
renderRbcomp(<React.Fragment>{as}</React.Fragment>, $('.announcement-wrapper'))
})
}
$(document).ready(() => $showAnnouncement())

View file

@ -3,7 +3,7 @@
/* global converEmoji, FeedsEditor */
const FeedsSorts = { newer: '最近发布', older: '最早发布', modified: '最近修改' }
const FeedsTypes = { 1: '动态', 2: '跟进' }
const FeedsTypes = { 1: '动态', 2: '跟进', 3: '公告' }
// ~
// eslint-disable-next-line no-unused-vars
@ -373,12 +373,21 @@ function __renderRichContent(e) {
//
const contentHtml = converEmoji(e.content.replace(/\n/g, '<br>'))
return <div className="rich-content">
<div className="texts"
<div className="texts text-break"
dangerouslySetInnerHTML={{ __html: contentHtml }}
/>
{e.related && <div className="related">
<span className="text-muted"><i className={`icon zmdi zmdi-${e.related.icon}`} /> {e.related.entityLabel} - </span>
<a target="_blank" href={`${rb.baseUrl}/app/list-and-view?id=${e.related.id}`}>{e.related.text}</a>
{e.related && <div className="mores">
<div>
<span><i className={`icon zmdi zmdi-${e.related.icon}`} /> {e.related.entityLabel} : </span>
<a target="_blank" href={`${rb.baseUrl}/app/list-and-view?id=${e.related.id}`} title="查看相关记录">{e.related.text}</a>
</div>
</div>
}
{e.type === 3 && <div className="mores">
{e.contentMore.showWhere > 0
&& <div><span>公示位置 : </span> {__findMaskTexts(e.contentMore.showWhere, ANN_OPTIONS).join('、')}</div>}
{(e.contentMore.timeStart || e.contentMore.timeEnd)
&& <div><span>公示时间 : </span> {e.contentMore.timeStart || ''} {e.contentMore.timeEnd}</div>}
</div>
}
{(e.images || []).length > 0 && <div className="img-field">
@ -403,6 +412,15 @@ function __renderRichContent(e) {
</div>
}
const ANN_OPTIONS = [[1, '动态页'], [2, '首页'], [4, '登录页']]
function __findMaskTexts(mask, options) {
let texts = []
options.forEach((item) => {
if ((item[0] & mask) !== 0) texts.push(item[1])
})
return texts
}
//
function _handleLike(id, comp) {
event.preventDefault()

View file

@ -7,18 +7,24 @@ class FeedsPost extends React.Component {
state = { ...this.props, type: 1 }
render() {
const activeType = this.state.type
const activeClass = 'text-primary text-bold'
return <div className="feeds-post">
<ul className="list-unstyled list-inline mb-1 pl-1">
<ul className="list-unstyled list-inline mb-1 pl-1" ref={(c) => this._activeType = c}>
<li className="list-inline-item">
<a onClick={() => this.setState({ type: 1 })} className={`${this.state.type === 1 && 'text-primary'}`}>动态</a>
<a onClick={() => this.setState({ type: 1 })} className={`${activeType === 1 ? activeClass : ''}`}>动态</a>
</li>
<li className="list-inline-item">
<a onClick={() => this.setState({ type: 2 })} className={`${this.state.type === 2 && 'text-primary'}`}>跟进</a>
<a onClick={() => this.setState({ type: 2 })} className={`${activeType === 2 ? activeClass : ''}`}>跟进</a>
</li>
{rb.isAdminUser && <li className="list-inline-item">
<a onClick={() => this.setState({ type: 3 })} className={`${activeType === 3 ? activeClass : ''}`}>公告</a>
</li>
}
</ul>
<div className="arrow_box" style={{ marginLeft: this.state.type === 2 ? 53 : 8 }}></div>
<div className="arrow_box" ref={(c) => this._activeArrow = c}></div>
<div>
<FeedsEditor ref={(c) => this._editor = c} type={this.state.type} />
<FeedsEditor ref={(c) => this._editor = c} type={activeType} />
</div>
<div className="mt-3">
<div className="float-right">
@ -39,6 +45,13 @@ class FeedsPost extends React.Component {
</div>
}
componentDidUpdate(prevProps, prevState) {
if (prevState.type !== this.state.type) {
let pos = $(this._activeType).find('.text-primary').position()
$(this._activeArrow).css('margin-left', pos.left - 31)
}
}
componentDidMount = () => $('#rb-feeds').attr('class', '')
_selectScope = (e) => {
@ -61,12 +74,15 @@ class FeedsPost extends React.Component {
_post = () => {
let _data = this._editor.vals()
if (!_data) return
if (!_data.content) { RbHighbar.create('请输入动态内容'); return }
_data.scope = this.state.scope
if (_data.scope === 'GROUP') {
if (!this.__group) { RbHighbar.create('请选择团队'); return }
_data.scope = this.__group.id
}
_data.type = this.state.type
_data.metadata = { entity: 'Feeds', id: this.props.id }
@ -107,7 +123,7 @@ class FeedsEditor extends React.Component {
}
return (<React.Fragment>
<div className={`rich-editor ${this.state.focus && 'active'}`}>
<div className={`rich-editor ${this.state.focus ? 'active' : ''}`}>
<textarea ref={(c) => this._editor = c} placeholder={this.props.placeholder} maxLength="2000"
onFocus={() => this.setState({ focus: true })}
onBlur={() => this.setState({ focus: false })}
@ -135,7 +151,8 @@ class FeedsEditor extends React.Component {
</ul>
</div>
</div>
{this.state.type === 2 && <SelectRelated ref={(c) => this._selectRelated = c} initValue={this.props.related} />}
{this.state.type === 2 && <SelectRelated ref={(c) => this._selectRelated = c} initValue={this.state.related} />}
{this.state.type === 3 && <AnnouncementOptions ref={(c) => this._announcementOptions = c} initValue={this.state.contentMore} />}
{((this.state.images || []).length > 0 || (this.state.files || []).length > 0) && <div className="attachment">
<div className="img-field">
{(this.state.images || []).map((item) => {
@ -239,6 +256,10 @@ class FeedsEditor extends React.Component {
attachments: this.state.files
}
if (this.state.type === 2 && this._selectRelated) vals.relatedRecord = this._selectRelated.val()
else if (this.state.type === 3 && this._announcementOptions) {
vals.contentMore = this._announcementOptions.val()
if (!vals.contentMore) return
}
return vals
}
focus = () => $(this._editor).selectRange(9999, 9999) // Move to last
@ -246,6 +267,7 @@ class FeedsEditor extends React.Component {
$(this._editor).val('')
autosize.update(this._editor)
if (this._selectRelated) this._selectRelated.reset()
if (this._announcementOptions) this._announcementOptions.reset()
this.setState({ files: null, images: null })
}
}
@ -330,8 +352,8 @@ class SelectRelated extends React.Component {
//
if (this.props.initValue) {
$(this._entity).val(this.props.initValue[4]).trigger('change')
let option = new Option(this.props.initValue[1], this.props.initValue[0], true, true)
$(this._entity).val(this.props.initValue.entity).trigger('change')
let option = new Option(this.props.initValue.text, this.props.initValue.id, true, true)
$(this._record).append(option)
}
})
@ -367,19 +389,96 @@ class SelectRelated extends React.Component {
reset = () => $(this._record).val(null).trigger('change')
}
const EMOJIS = { '赞': 'fs_zan.png', '握手': 'fs_woshou.png', '耶': 'fs_ye.png', '抱拳': 'fs_baoquan.png', 'OK': 'fs_ok.png', '拍手': 'fs_paishou.png', '拜托': 'fs_baituo.png', '差评': 'fs_chaping.png', '微笑': 'fs_weixiao.png', '撇嘴': 'fs_piezui.png', '花痴': 'fs_huachi.png', '发呆': 'fs_fadai.png', '得意': 'fs_deyi.png', '大哭': 'fs_daku.png', '害羞': 'fs_haixiu.png', '闭嘴': 'fs_bizui.png', '睡着': 'fs_shuizhao.png', '敬礼': 'fs_jingli.png', '崇拜': 'fs_chongbai.png', '抱抱': 'fs_baobao.png', '忍住不哭': 'fs_renzhubuku.png', '尴尬': 'fs_ganga.png', '发怒': 'fs_fanu.png', '调皮': 'fs_tiaopi.png', '开心': 'fs_kaixin.png', '惊讶': 'fs_jingya.png', '呵呵': 'fs_hehe.png', '思考': 'fs_sikao.png', '哭笑不得': 'fs_kuxiaobude.png', '抓狂': 'fs_zhuakuang.png', '呕吐': 'fs_outu.png', '偷笑': 'fs_touxiao.png', '笑哭了': 'fs_xiaokule.png', '白眼': 'fs_baiyan.png', '傲慢': 'fs_aoman.png', '饥饿': 'fs_jie.png', '困': 'fs_kun.png', '吓': 'fs_xia.png', '流汗': 'fs_liuhan.png', '憨笑': 'fs_hanxiao.png', '悠闲': 'fs_youxian.png', '奋斗': 'fs_fendou.png', '咒骂': 'fs_zhouma.png', '疑问': 'fs_yiwen.png', '嘘': 'fs_xu.png', '晕': 'fs_yun.png', '惊恐': 'fs_jingkong.png', '衰': 'fs_shuai.png', '骷髅': 'fs_kulou.png', '敲打': 'fs_qiaoda.png', '再见': 'fs_zaijian.png', '无语': 'fs_wuyu.png', '抠鼻': 'fs_koubi.png', '鼓掌': 'fs_guzhang.png', '糗大了': 'fs_qiudale.png', '猥琐的笑': 'fs_weisuodexiao.png', '哼': 'fs_heng.png', '不爽': 'fs_bushuang.png', '打哈欠': 'fs_dahaqian.png', '鄙视': 'fs_bishi.png', '委屈': 'fs_weiqu.png', '安慰': 'fs_anwei.png', '坏笑': 'fs_huaixiao.png', '亲亲': 'fs_qinqin.png', '冷汗': 'fs_lenghan.png', '可怜': 'fs_kelian.png', '生病': 'fs_shengbing.png', '愉快': 'fs_yukuai.png', '幸灾乐祸': 'fs_xingzailehuo.png', '大便': 'fs_dabian.png', '干杯': 'fs_ganbei.png', '钱': 'fs_qian.png' }
// eslint-disable-next-line no-unused-vars
const converEmoji = function (text) {
let es = text.match(/\[(.+?)\]/g)
if (!es) return text
es.forEach((e) => {
let img = EMOJIS[e.substr(1, e.length - 2)]
if (img) {
img = `<img class="emoji" src="${rb.baseUrl}/assets/img/emoji/${img}"/>`
text = text.replace(e, img)
//
class AnnouncementOptions extends React.Component {
state = { ...this.props }
render() {
return <div className="announcement-options">
<dl className="row mb-1">
<dt className="col-12 col-lg-3">同时公示在</dt>
<dd className="col-12 col-lg-9 mb-0" ref={(c) => this._showWhere = c}>
<label className="custom-control custom-checkbox custom-control-inline">
<input className="custom-control-input" name="showOn" type="checkbox" value={1} disabled={this.props.readonly} />
<span className="custom-control-label">动态页</span>
</label>
<label className="custom-control custom-checkbox custom-control-inline">
<input className="custom-control-input" name="showOn" type="checkbox" value={2} disabled={this.props.readonly} />
<span className="custom-control-label">首页</span>
</label>
<label className="custom-control custom-checkbox custom-control-inline">
<input className="custom-control-input" name="showOn" type="checkbox" value={4} disabled={this.props.readonly} />
<span className="custom-control-label">登录页 <i className="zmdi zmdi-help zicon down-3" data-toggle="tooltip" title="选择登录页公示请注意不要发布敏感信息" /></span>
</label>
</dd>
</dl>
<dl className="row">
<dt className="col-12 col-lg-3 pt-2">公示时间</dt>
<dd className="col-12 col-lg-9" ref={(c) => this._showTime = c}>
<div className="input-group">
<input type="text" className="form-control form-control-sm" placeholder="现在" />
<div className="input-group-prepend input-group-append">
<span className="input-group-text"></span>
</div>
<input type="text" className="form-control form-control-sm" placeholder="选择结束时间" />
</div>
</dd>
</dl>
</div>
}
componentDidMount() {
$(this._showTime).find('.form-control').datetimepicker({
componentIcon: 'zmdi zmdi-calendar',
navIcons: {
rightIcon: 'zmdi zmdi-chevron-right',
leftIcon: 'zmdi zmdi-chevron-left'
},
format: 'yyyy-mm-dd hh:ii:ss',
minView: 0,
weekStart: 1,
autoclose: true,
language: 'zh',
showMeridian: false,
keyboardNavigation: false,
minuteStep: 5
})
$(this._showWhere).find('.zicon').tooltip()
const initValue = this.props.initValue
if (initValue) {
$(this._showTime).find('.form-control:eq(0)').val(initValue.timeStart || '')
$(this._showTime).find('.form-control:eq(1)').val(initValue.timeEnd || '')
$(this._showWhere).find('input').each(function () {
if ((~~$(this).val() & initValue.showWhere) !== 0) $(this).prop('checked', true)
})
}
})
return text.replace(/\n/g, '<br />')
}
componentWillUnmount() {
$(this._showTime).find('.form-control').datetimepicker('remove')
}
val() {
let timeStart = $(this._showTime).find('.form-control:eq(0)').val()
let timeEnd = $(this._showTime).find('.form-control:eq(1)').val()
if (!timeEnd) {
RbHighbar.create('请选择结束时间')
return
}
let where = 0
$(this._showWhere).find('input:checked').each(function () { where += ~~$(this).val() })
return {
timeStart: timeStart || null,
timeEnd: timeEnd,
showWhere: where
}
}
reset() {
$(this._showTime).find('.form-control').val('')
$(this._showWhere).find('input').prop('checked', false)
}
}
// ~~
@ -395,7 +494,8 @@ class FeedsEditDlg extends RbModalHandler {
type: this.props.type,
images: this.props.images,
files: this.props.attachments,
related: this.props.related
related: this.props.related,
contentMore: this.props.contentMore
}
return <RbModal ref={(c) => this._dlg = c} title="编辑动态" disposeOnHide={true}>
<div className="m-1"><FeedsEditor ref={(c) => this._editor = c} {..._data} /></div>

View file

@ -303,7 +303,7 @@ class RbFormElement extends React.Component {
}
const editable = EDIT_ON_VIEW && props.onView && !props.readonly
return <div className={`form-group row type-${props.type} ${editable ? 'editable' : ''}`} data-field={props.field}>
<label ref={(c) => this._fieldLabel = c} className={`col-12 col-form-label text-sm-right col-sm-${colWidths[0]} ${!props.onView && !props.nullable && 'required'}`}>{props.label}</label>
<label ref={(c) => this._fieldLabel = c} className={`col-12 col-sm-${colWidths[0]} col-form-label text-sm-right ${(!props.onView && !props.nullable) ? 'required' : ''}`}>{props.label}</label>
<div ref={(c) => this._fieldText = c} className={'col-12 col-sm-' + colWidths[1]}>
{(!props.onView || (editable && this.state.editMode)) ? this.renderElement() : this.renderViewElement()}
{!props.onView && props.tip && <p className="form-text">{props.tip}</p>}
@ -950,7 +950,7 @@ class RbFormMultiSelect extends RbFormElement {
return <div className="mt-1" ref={(c) => this._fieldValue__wrap = c}>
{(this.props.options || []).length === 0 && <div className="text-danger">选项未配置</div>}
{(this.props.options || []).map((item) => {
return <label key={name + item.mask} className="custom-control custom-checkbox custom-control-inline">
return <label key={name + item.mask} className="custom-control custom-checkbox custom-control-inline">
<input className="custom-control-input" name={name} type="checkbox" checked={(this.state.value & item.mask) !== 0} value={item.mask}
onChange={this.changeValue} disabled={this.props.readonly} />
<span className="custom-control-label">{item.text}</span>

View file

@ -31,11 +31,11 @@ $(function () {
setTimeout(__globalSearch, 200)
}
if (rb.isAdminUser === true) {
if (rb.isAdminUser) {
$('html').addClass('admin')
if (rb.isAdminVerified !== true) $('.admin-verified').remove()
if (location.href.indexOf('/admin/') > -1) $('.admin-settings').remove()
else if (rb.isAdminVerified === true) $('.admin-settings a i').addClass('text-danger')
else if (rb.isAdminVerified) $('.admin-settings a i').addClass('text-danger')
} else {
$('.admin-show').remove()
}
@ -458,4 +458,4 @@ var $pgt = {
RecordList: 'RecordList',
SlaveView: 'SlaveView',
SlaveList: 'SlaveList'
}
}

View file

@ -20,6 +20,7 @@
<%@ include file="/_include/spinner.jsp"%>
</div>
<div class="rb-content">
<div class="announcement-wrapper"></div>
<div class="main-content container-fluid p-0">
<div class="tools-bar">
<div class="row">
@ -59,5 +60,6 @@
<script src="${baseUrl}/assets/js/rb-approval.jsx" type="text/babel"></script>
<script src="${baseUrl}/assets/js/charts/dashboard.jsx" type="text/babel"></script>
<script src="${baseUrl}/assets/js/settings-share2.jsx" type="text/babel"></script>
<script src="${baseUrl}/assets/js/feeds/announcement.jsx" type="text/babel"></script>
</body>
</html>

View file

@ -15,6 +15,7 @@
<jsp:param value="nav_entity-Feeds" name="activeNav"/>
</jsp:include>
<div class="rb-content">
<div class="announcement-wrapper"></div>
<div class="main-content container container-smart">
<div class="row">
<div class="col-lg-8 col-12">
@ -65,6 +66,7 @@
<ul class="list-unstyled">
<li data-type="1"><a>动态</a></li>
<li data-type="2"><a>跟进</a></li>
<li data-type="3"><a>公告</a></li>
</ul>
</div>
</div>
@ -102,6 +104,7 @@
</div>
<%@ include file="/_include/Foot.jsp"%>
<script src="${baseUrl}/assets/lib/jquery.textarea.js"></script>
<script src="${baseUrl}/assets/js/feeds/announcement.jsx" type="text/babel"></script>
<script src="${baseUrl}/assets/js/feeds/feeds-post.jsx" type="text/babel"></script>
<script src="${baseUrl}/assets/js/feeds/feeds-list.jsx" type="text/babel"></script>
<script src="${baseUrl}/assets/js/feeds/feeds.jsx" type="text/babel"></script>

View file

@ -63,6 +63,7 @@
<div class="rb-wrapper rb-login">
<div class="rb-bgimg"></div>
<div class="rb-content">
<div class="announcement-wrapper"></div>
<div class="main-content container-fluid">
<div class="splash-container mb-0">
<div class="card card-border-color card-border-color-primary">
@ -175,5 +176,6 @@ $(document).ready(function() {
}
})
</script>
<script src="${baseUrl}/assets/js/feeds/announcement.jsx" type="text/babel"></script>
</body>
</html>

View file

@ -23,6 +23,7 @@ import cn.devezhao.persist4j.PersistManagerFactory;
import cn.devezhao.persist4j.engine.PersistManagerFactoryImpl;
import cn.devezhao.persist4j.metadata.impl.ConfigurationMetadataFactory;
import cn.devezhao.persist4j.util.support.Table;
import com.rebuild.server.metadata.EntityHelper;
import org.dom4j.Element;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
@ -45,7 +46,8 @@ public class SchemaGen {
CTX = new ClassPathXmlApplicationContext(new String[] { "application-ctx.xml", });
PMF = CTX.getBean(PersistManagerFactoryImpl.class);
genAll();
// genAll();
gen(EntityHelper.Feeds);
System.exit(0);
}

View file

@ -23,6 +23,7 @@ import cn.devezhao.persist4j.engine.ID;
import com.rebuild.server.Application;
import com.rebuild.server.TestSupportWithUser;
import com.rebuild.server.metadata.EntityHelper;
import com.rebuild.server.service.bizz.UserService;
import org.junit.Assert;
import org.junit.Test;
@ -54,6 +55,11 @@ public class FeedsHelperTest extends TestSupportWithUser {
FeedsHelper.checkReadable(feedsId, SIMPLE_USER);
}
@Test
public void formatContent() {
FeedsHelper.formatContent("123 @" + UserService.ADMIN_USER);
}
private ID createFeeds() {
Record feeds = EntityHelper.forNew(EntityHelper.Feeds, SIMPLE_USER);
feeds.setString("content", "你好,测试动态 @RB示例用户 @admin");

View file

@ -23,7 +23,7 @@ import org.junit.Test;
import java.util.Locale;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
/**
* @author devezhao
@ -37,8 +37,10 @@ public class LanguagesTest extends TestSupport {
System.out.println(Languages.instance.getCurrentBundle());
System.out.println(Languages.instance.getBundle(Locale.getDefault()));
assertEquals(Locale.US.toString(), Languages.instance.getBundle(Locale.US).locale());
assertEquals(Locale.JAPAN.toString(), Languages.instance.getBundle(Locale.JAPAN).locale());
assertEquals(Locale.US,
Locale.forLanguageTag(Languages.instance.getBundle(Locale.US).locale()));
assertEquals(Locale.JAPAN,
Locale.forLanguageTag(Languages.instance.getBundle(Locale.JAPAN).locale()));
}
@Test

View file

@ -23,8 +23,9 @@ import com.rebuild.server.TestSupport;
import com.rebuild.server.service.bizz.privileges.User;
import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
@ -56,25 +57,30 @@ public class UserStoreTest extends TestSupport {
@Test
public void testExists() throws Exception {
assertTrue(Application.getUserStore().exists("admin"));
assertTrue(!Application.getUserStore().exists("not_exists"));
assertTrue(Application.getUserStore().existsUser("admin"));
assertFalse(Application.getUserStore().existsUser("not_exists"));
}
@Test
public void testMemberToTeam() {
Application.getSessionStore().set(SIMPLE_USER);
try {
Application.getBean(TeamService.class).createMembers(SIMPLE_TEAM, Arrays.asList(SIMPLE_USER));
Application.getBean(TeamService.class).createMembers(SIMPLE_TEAM, Collections.singletonList(SIMPLE_USER));
User user = Application.getUserStore().getUser(SIMPLE_USER);
System.out.println(user.getOwningTeams());
assertTrue(!user.getOwningTeams().isEmpty());
assertFalse(user.getOwningTeams().isEmpty());
assertTrue(Application.getUserStore().getTeam(SIMPLE_TEAM).isMember(SIMPLE_USER));
Application.getBean(TeamService.class).deleteMembers(SIMPLE_TEAM, Arrays.asList(SIMPLE_USER));
Application.getBean(TeamService.class).deleteMembers(SIMPLE_TEAM, Collections.singletonList(SIMPLE_USER));
System.out.println(user.getOwningTeams());
} finally {
Application.getSessionStore().clean();
}
}
@Test
public void existsAny() {
Application.getUserStore().existsAny(RoleService.ADMIN_ROLE);
}
}