Merge pull request #342 from getrebuild/mytasks-685

Mytasks 685
This commit is contained in:
devezhao 2021-05-27 20:24:30 +08:00 committed by GitHub
commit 2d4b57d313
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 280 additions and 59 deletions

View file

@ -20,6 +20,7 @@ import com.rebuild.core.service.dashboard.ChartManager;
import com.rebuild.core.service.dashboard.charts.builtin.ApprovalList;
import com.rebuild.core.service.dashboard.charts.builtin.BuiltinChart;
import com.rebuild.core.service.dashboard.charts.builtin.FeedsSchedule;
import com.rebuild.core.service.dashboard.charts.builtin.ProjectTasks;
import com.rebuild.core.support.i18n.Language;
/**
@ -98,7 +99,8 @@ public class ChartsFactory {
public static BuiltinChart[] getBuiltinCharts() {
return new BuiltinChart[]{
new ApprovalList(),
new FeedsSchedule()
new FeedsSchedule(),
new ProjectTasks()
};
}
}

View file

@ -0,0 +1,79 @@
/*
Copyright (c) REBUILD <https://getrebuild.com/> and/or its owners. All rights reserved.
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
package com.rebuild.core.service.dashboard.charts.builtin;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.rebuild.core.Application;
import com.rebuild.core.configuration.ConfigBean;
import com.rebuild.core.service.dashboard.charts.ChartData;
import com.rebuild.core.service.project.ProjectManager;
import com.rebuild.core.support.i18n.I18nUtils;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.JSONUtils;
import java.util.Date;
/**
* @author devezhao
* @since 2021/5/26
*/
public class ProjectTasks extends ChartData implements BuiltinChart {
// 虚拟ID
public static final ID MYID = ID.valueOf("017-9000000000000003");
public ProjectTasks() {
super(null);
this.config = getChartConfig();
}
@Override
public ID getChartId() {
return MYID;
}
@Override
public String getChartTitle() {
return Language.L("我的任务");
}
@Override
public JSON build() {
final int viewState = ObjectUtils.toInt(getExtraParams().get("state"), 0);
Object[][] tasks = Application.createQueryNoFilter(
"select taskId,projectId,projectPlanId,taskNumber,taskName,createdOn,deadline,endTime,status,priority" +
" from ProjectTask where executor = ? and status = ? order by seq asc")
.setParameter(1, getUser())
.setParameter(2, viewState)
.array();
JSONArray array = new JSONArray();
for (Object[] o : tasks) {
ID projectId = (ID) o[1];
ConfigBean cbProject = ProjectManager.instance.getProject(projectId, null);
ConfigBean cbPlan = ProjectManager.instance.getPlanOfProject((ID) o[2], projectId);
String projectName = String.format("%s (%s)",
cbProject.getString("projectName"), cbPlan.getString("planName"));
String taskNumber = String.format("%s-%s", cbProject.getString("projectCode"), o[3]);
array.add(JSONUtils.toJSONObject(
new String[] { "id", "projectName", "planFlow", "taskNumber", "taskName", "createdOn", "deadline", "endTime", "status", "priority" },
new Object[] { o[0], projectName, cbPlan.getInteger("flowStatus"), taskNumber, o[4],
I18nUtils.formatDate((Date) o[5]),
I18nUtils.formatDate((Date) o[6]),
I18nUtils.formatDate((Date) o[7]),
o[8], o[9]
}));
}
return array;
}
}

View file

@ -134,12 +134,12 @@ public class ProjectManager implements ConfigManager {
* 获取指定项目
*
* @param projectId
* @param user
* @param checkUser
* @return
* @throws ConfigurationException If not found
*/
public ConfigBean getProject(ID projectId, ID user) throws ConfigurationException {
ConfigBean[] ee = user == null ? getAllProjects() : getAvailable(user);
public ConfigBean getProject(ID projectId, ID checkUser) throws ConfigurationException {
ConfigBean[] ee = checkUser == null ? getAllProjects() : getAvailable(checkUser);
for (ConfigBean e : ee) {
if (projectId.equals(e.getID("id"))) {
return e.clone();

View file

@ -266,7 +266,8 @@ public class ProjectTaskController extends BaseController {
ID projectId = (ID) o[12];
ConfigBean project = ProjectManager.instance.getProject(projectId, null);
ConfigBean plan = ProjectManager.instance.getPlanOfProject(projectPlanId, projectId);
formatted.put("planName", String.format("%s (%s)", plan.getString("planName"), project.getString("projectName")));
formatted.put("planName", String.format("%s (%s)",
project.getString("projectName"), plan.getString("planName")));
formatted.put("planFlow", plan.getInteger("flowStatus"));
alist.add(formatted);

View file

@ -15,7 +15,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
}
.chart-box.INDEX {
padding-bottom: 0px;
padding-bottom: 0;
}
.chart-box.high {
@ -152,7 +152,6 @@ See LICENSE and COMMERCIAL in the project root for license information.
}
.chart.ctable .table th {
background-color: #dee2e6;
background-color: #eceff1;
border-width: 1px;
font-weight: normal;
@ -168,29 +167,28 @@ See LICENSE and COMMERCIAL in the project root for license information.
}
.chart-box.ApprovalList,
.chart-box.FeedsSchedule {
padding: 0;
padding-top: 20px;
.chart-box.FeedsSchedule,
.chart-box.ProjectTasks {
padding: 20px 0 0;
}
.chart-box.ApprovalList .chart-head,
.chart-box.FeedsSchedule .chart-head {
.chart-box.FeedsSchedule .chart-head,
.chart-box.ProjectTasks .chart-head {
margin: 0 25px;
}
.chart.ApprovalList,
.chart.FeedsSchedule {
.chart.FeedsSchedule,
.chart.ProjectTasks {
position: relative;
width: 100%;
margin-top: 5px;
}
.chart.FeedsSchedule {
margin-top: 13px;
}
.chart.ApprovalList .table th,
.chart.FeedsSchedule .table th {
.chart.FeedsSchedule .table th,
.chart.ProjectTasks .table th {
border-top: 0 none;
padding-top: 0;
padding-bottom: 6px;
@ -198,21 +196,26 @@ See LICENSE and COMMERCIAL in the project root for license information.
}
.chart.ApprovalList .table td,
.chart.FeedsSchedule .table td {
.chart.FeedsSchedule .table td,
.chart.ProjectTasks .table td {
padding: 8px 10px;
}
.chart.ApprovalList .table tr th:first-child,
.chart.ApprovalList .table tr td:first-child,
.chart.FeedsSchedule .table tr th:first-child,
.chart.FeedsSchedule .table tr td:first-child {
.chart.FeedsSchedule .table tr td:first-child,
.chart.ProjectTasks .table tr th:first-child,
.chart.ProjectTasks .table tr td:first-child {
padding-left: 25px;
}
.chart.ApprovalList .table tr th:last-child,
.chart.ApprovalList .table tr td:last-child,
.chart.FeedsSchedule .table tr th:last-child,
.chart.FeedsSchedule .table tr td:last-child {
.chart.FeedsSchedule .table tr td:last-child,
.chart.ProjectTasks .table tr th:last-child,
.chart.ProjectTasks .table tr td:last-child {
padding-right: 23px;
}
@ -236,8 +239,14 @@ See LICENSE and COMMERCIAL in the project root for license information.
box-shadow: inset 3px 3px 3px rgba(0, 0, 0, 0.15);
}
.chart.FeedsSchedule td > .content {
max-height: 37px;
.chart.FeedsSchedule,
.chart.ProjectTasks {
margin-top: 13px;
}
.chart.FeedsSchedule td > .content,
.chart.ProjectTasks td > .content {
max-height: 36px;
overflow: hidden;
text-overflow: ellipsis;
color: #404040;
@ -245,13 +254,31 @@ See LICENSE and COMMERCIAL in the project root for license information.
line-height: 1;
}
.chart.FeedsSchedule td > .content:hover {
.chart.FeedsSchedule td > .content:hover,
.chart.ProjectTasks td > .content:hover {
opacity: 0.8;
}
.chart.FeedsSchedule td > .content p {
.chart.FeedsSchedule td > .content p,
.chart.ProjectTasks td > .content p {
margin: 0;
line-height: 1.5;
line-height: 1.4;
}
.chart.ProjectTasks tr.priority-2 td:first-child,
.chart.ProjectTasks tr.priority-3 td:first-child {
border-left: 5px solid #f7b904;
padding-left: 20px;
}
.chart.ProjectTasks tr.priority-3 td:first-child {
border-left-color: #ea3f30;
}
.chart.ProjectTasks tr.status-1 a,
.chart.ProjectTasks tr.status-1 label {
color: #8c8c8c;
opacity: 0.5;
}
.chart-select-wrap .nav-pills .nav-link {
@ -314,7 +341,6 @@ See LICENSE and COMMERCIAL in the project root for license information.
.chart-list > div .delete {
font-size: 1.2rem;
color: #a1a1a1;
display: inline-block;
padding: 8px;
margin-top: 3px;
margin-right: 6px;

View file

@ -558,18 +558,6 @@ body {
margin: -1px 0 0;
}
.tasks-list.inview .header-title .title .badge {
background-color: #f5f5f5;
color: #595959;
font-size: 12px;
font-weight: normal;
padding: 0.07692rem 0.38rem;
border: 0 none;
float: left;
margin-top: -2px;
margin-right: 10px;
}
.tasks-list.inview .header-title::after {
content: '';
}
@ -600,6 +588,16 @@ body {
border-left-color: #ea3f30;
}
.tasks-list.inview .card.priority-2 .header-title .title,
.tasks-list.inview .card.priority-3 .header-title .title {
padding-left: 41px;
}
.tasks-list.inview .card.priority-2 .header-title .title .custom-control,
.tasks-list.inview .card.priority-3 .header-title .title .custom-control {
left: 11px;
}
.tasks-list.inview .card.status-1 .title {
color: #8c8c8c;
opacity: 0.5;

View file

@ -732,7 +732,7 @@ class FeedsSchedule extends BaseChart {
return (
<tr key={'schedule-' + idx}>
<td>
<a title={$L('查看详情')} href={`${rb.baseUrl}/app/list-and-view?id=${item.id}`} className="content text-break" dangerouslySetInnerHTML={{ __html: item.content }} />
<a href={`${rb.baseUrl}/app/list-and-view?id=${item.id}`} className="content text-break" dangerouslySetInnerHTML={{ __html: item.content }} />
</td>
<td className="cell-detail">
<div>{item.scheduleTime.substr(0, 16)}</div>
@ -761,6 +761,9 @@ class FeedsSchedule extends BaseChart {
.find('.FeedsSchedule')
.css('height', $tb.height() - 13)
.perfectScrollbar()
$tb.find('.content').each(function () {
$(this).attr('title', $(this).text())
})
this._$tb = $tb
})
return table
@ -958,6 +961,108 @@ class ChartScatter extends BaseChart {
}
}
// ~ 我的任务
class ProjectTasks extends BaseChart {
renderChart(data) {
const table =
(data || []).length === 0 ? (
<div className="chart-undata must-center">
<i className="zmdi zmdi-check icon text-success"></i> {$L('你已完成所有任务')}
</div>
) : (
<div>
<table className="table table-striped table-hover">
<thead>
<tr>
<th width="40"></th>
<th>{$L('任务')}</th>
<th style={{ minWidth: 150 }}>{$L('时间')}</th>
</tr>
</thead>
<tbody>
{data.map((item) => {
return (
<tr key={item.id} className={`status-${item.status} priority-${item.priority}`}>
<td className="align-text-top">
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input className="custom-control-input" type="checkbox" disabled={item.planFlow === 2} onClick={(e) => this._toggleStatus(item, e)} />
<span className="custom-control-label"></span>
</label>
</td>
<td>
<a title={item.taskName} href={`${rb.baseUrl}/app/list-and-view?id=${item.id}`} className="content">
<p className="text-break">
[{item.taskNumber}] {item.taskName}
</p>
</a>
<p className="text-muted fs-12 m-0" style={{ lineHeight: 1 }}>
{item.projectName}
</p>
</td>
<td className="text-muted">
{!item.deadline && !item.endTime && (
<React.Fragment>
<span className="mr-1">{$L('创建时间')}</span>
<DateShow date={item.createdOn} />
</React.Fragment>
)}
{item.endTime && (
<React.Fragment>
<span className="mr-1">{$L('完成时间')}</span>
<DateShow date={item.endTime} />
</React.Fragment>
)}
{!item.endTime && item.deadline && (
<React.Fragment>
<span className="mr-1">{$L('到期时间')}</span>
<DateShow date={item.deadline} />
</React.Fragment>
)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
const chartdata = <div className="chart ProjectTasks">{table}</div>
this.setState({ chartdata: chartdata }, () => {
const $tb = $(this._$body)
$tb
.find('.ProjectTasks')
.css('height', $tb.height() - 13)
.perfectScrollbar()
this._$tb = $tb
})
return table
}
resize() {
$setTimeout(
() => {
if (this._$tb) this._$tb.find('.ProjectTasks').css('height', this._$tb.height() - 13)
},
400,
'resize-chart-' + this.state.id
)
}
_toggleStatus(item, e) {
const $target = $(e.currentTarget)
const data = {
status: $target.prop('checked') ? 1 : 0,
metadata: { id: item.id },
}
$.post('/app/entity/common-save', JSON.stringify(data), (res) => {
if (res.error_code > 0) return RbHighbar.error(res.error_msg)
$target.parents('tr').removeClass('status-0 status-1').addClass('status-' + data.status)
})
}
}
// 确定图表类型
// eslint-disable-next-line no-unused-vars
const detectChart = function (cfg, id) {
@ -986,6 +1091,8 @@ const detectChart = function (cfg, id) {
return <ChartRadar {...props} />
} else if (cfg.type === 'SCATTER') {
return <ChartScatter {...props} />
} else if (cfg.type === 'ProjectTasks') {
return <ProjectTasks {...props} builtin={true} />
} else {
return <h4 className="chart-undata must-center">{`${$L('未知图表')} [${cfg.type}]`}</h4>
}

View file

@ -212,7 +212,8 @@ const render_dashboard = function (init) {
// When resize/re-postion/remove
$('.grid-stack')
.on('change', function () {
.on('change', function (e) {
if (e.target) return // input 元素也会触发 ???
save_dashboard()
})
.on('resizestart', function () {

View file

@ -279,6 +279,7 @@ class ValueStatus extends ValueComp {
}
// 执行者
// TODO 执行者仅允许选择成员 ???
class ValueExecutor extends ValueComp {
state = { ...this.props }
@ -1066,7 +1067,7 @@ class TextEditor extends React.Component {
const item = EMOJIS[k]
this.__es.push(
<a key={`em-${item}`} title={k} onClick={() => this._selectEmoji(k)}>
<img src={`${rb.baseUrl}/assets/img/emoji/${item}`} />
<img src={`${rb.baseUrl}/assets/img/emoji/${item}`} alt={k} />
</a>
)
}

View file

@ -91,7 +91,6 @@ class LightFeedsList extends RelatedList {
class LightTaskList extends RelatedList {
constructor(props) {
super(props)
this.__FeedsList = new FeedsList()
this.__listClass = 'tasks-list inview'
this.__listNoData = (
@ -128,12 +127,18 @@ class LightTaskList extends RelatedList {
<span className="custom-control-label"></span>
</label>
<a href={`${rb.baseUrl}/app/list-and-view?id=${item.id}`} target="_blank" title={$L('打开')}>
<span className="badge">{item.taskNumber}</span>
{item.taskName}
[{item.taskNumber}] {item.taskName}
</a>
</div>
<div className="col-5 task-meta">
<div className="row">
<div className="col-2">
{item.executor && (
<a className="avatar" title={$L('执行人')}>
<img src={`${rb.baseUrl}/account/user-avatar/${item.executor[0]}`} title={item.executor[1]} alt="Avatar" />
</a>
)}
</div>
<div className="col-5 text-ellipsis">{item.planName}</div>
<div className="col-5 text-ellipsis">
{!item.deadline && !item.endTime && (
@ -142,24 +147,17 @@ class LightTaskList extends RelatedList {
<DateShow date={item.createdOn} />
</React.Fragment>
)}
{item.deadline && (
<React.Fragment>
<span className="mr-1">{$L('到期时间')}</span>
<DateShow date={item.deadline} />
</React.Fragment>
)}
{!item.deadline && item.endTime && (
{item.endTime && (
<React.Fragment>
<span className="mr-1">{$L('完成时间')}</span>
<DateShow date={item.endTime} />
</React.Fragment>
)}
</div>
<div className="col-2">
{item.executor && (
<a className="avatar">
<img src={`${rb.baseUrl}/account/user-avatar/${item.executor[0]}`} title={item.executor[1]} alt="Avatar" />
</a>
{!item.endTime && item.deadline && (
<React.Fragment>
<span className="mr-1">{$L('到期时间')}</span>
<DateShow date={item.deadline} />
</React.Fragment>
)}
</div>
</div>

View file

@ -12,6 +12,7 @@ import com.rebuild.TestSupport;
import com.rebuild.core.service.approval.ApprovalState;
import com.rebuild.core.service.dashboard.charts.builtin.ApprovalList;
import com.rebuild.core.service.dashboard.charts.builtin.FeedsSchedule;
import com.rebuild.core.service.dashboard.charts.builtin.ProjectTasks;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
@ -24,16 +25,23 @@ import java.util.Map;
public class BuiltinChartsTest extends TestSupport {
@Test
public void testApprovalList() {
void testApprovalList() {
Map<String, Object> params = new HashMap<>();
params.put("state", ApprovalState.APPROVED.getState());
JSON ret = ((ApprovalList) new ApprovalList().setUser(SIMPLE_USER)).setExtraParams(params).build();
System.out.println(ret);
}
@Test
public void testFeedsSchedule() {
void testFeedsSchedule() {
JSON ret = ((FeedsSchedule) new FeedsSchedule().setUser(SIMPLE_USER)).build();
System.out.println(ret);
}
@Test
void testProjectTasks() {
JSON ret = ((ProjectTasks) new ProjectTasks().setUser(SIMPLE_USER)).build();
System.out.println(ret);
}
}