relatedRecord in feed

This commit is contained in:
devezhao 2021-04-07 15:04:13 +08:00
parent 24276501a8
commit 11655cf802
15 changed files with 232 additions and 132 deletions

View file

@ -110,5 +110,6 @@ module.exports = {
$isTrue: true,
$fieldIsCompatible: true,
$unhideDropdown: true,
AnyRecordSelector: true,
},
}

View file

@ -19,6 +19,7 @@ import com.rebuild.core.privileges.UserHelper;
import com.rebuild.core.service.project.ProjectHelper;
import com.rebuild.core.service.project.ProjectManager;
import com.rebuild.core.service.query.AdvFilterParser;
import com.rebuild.core.support.general.FieldValueHelper;
import com.rebuild.core.support.i18n.I18nUtils;
import com.rebuild.utils.JSONUtils;
import com.rebuild.web.BaseController;
@ -146,7 +147,7 @@ public class ProjectTaskController extends BaseController {
@GetMapping("tasks/details")
public JSON taskDetails(@IdParam(name = "task") ID taskId) {
Object[] task = Application.createQueryNoFilter(
"select " + BASE_FIELDS + ",projectId,description,attachments from ProjectTask where taskId = ?")
"select " + BASE_FIELDS + ",projectId,description,attachments,relatedRecord from ProjectTask where taskId = ?")
.setParameter(1, taskId)
.unique();
JSONObject details = formatTask(task, true);
@ -157,6 +158,14 @@ public class ProjectTaskController extends BaseController {
String attachments = (String) task[13];
details.put("attachments", JSON.parseArray(attachments));
// 相关记录
ID relatedRecord = (ID) task[14];
if (relatedRecord != null) {
details.put("relatedRecord", relatedRecord);
String text = FieldValueHelper.getLabelNotry(relatedRecord);
details.put("relatedRecordData", FieldValueHelper.wrapMixValue(relatedRecord, text));
}
return details;
}

View file

@ -202,8 +202,7 @@
"DeleteSomeConfirm": "Confirm to delete this {0}?",
"RemoveSomeConfirm": "Confirm to remove this {0}?",
"DeleteCasTips": "Delete associated records at the same time",
"ClickViewReleated": "Click to view related record",
"ReleatedRecord": "Related record",
"ClickViewReleated": "Click to view record",
"NotAllow": "Not Allow",
"Allow": "Allow",
"AllowSome": "Allow {0}",

View file

@ -202,8 +202,7 @@
"DeleteSomeConfirm": "确认删除此{0}",
"RemoveSomeConfirm": "确认移除此{0}",
"DeleteCasTips": "同时删除关联记录",
"ClickViewReleated": "点击查看相关记录",
"ReleatedRecord": "相关记录",
"ClickViewReleated": "点击查看记录",
"NotAllow": "不允许",
"Allow": "允许",
"AllowSome": "允许{0}",

View file

@ -202,8 +202,7 @@
"DeleteSomeConfirm": "確認删除此{0}",
"RemoveSomeConfirm": "確認移除此{0}",
"DeleteCasTips": "同時删除關聯記錄",
"ClickViewReleated": "點擊查看相關記錄",
"ReleatedRecord": "相關記錄",
"ClickViewReleated": "點擊查看記錄",
"NotAllow": "不允許",
"Allow": "允許",
"AllowSome": "允許{0}",

View file

@ -451,9 +451,11 @@
<field name="description" type="text" description="备注" queryable="false"/>
<field name="attachments" type="string" max-length="700" description="附件" extra-attrs="{displayType:'FILE'}"/>
<field name="parentTaskId" type="reference" ref-entity="ProjectTask" description="父级任务" queryable="false"/>
<field name="relatedRecord" type="any-reference" description="相关业务记录" cascade="ignore"/>
<field name="seq" type="int" default-value="0" description="排序 (小到大)" queryable="false"/>
<index field-list="projectId,projectPlanId,seq"/>
<index field-list="projectId,taskNumber,taskName,status"/>
<index field-list="relatedRecord,projectId"/>
</entity>
<entity name="ProjectTaskRelation" type-code="053" description="任务关系" queryable="false" parent="false">

View file

@ -646,6 +646,7 @@ create table if not exists `project_task` (
`DESCRIPTION` text(32767) comment '备注',
`ATTACHMENTS` varchar(700) comment '附件',
`PARENT_TASK_ID` char(20) comment '父级任务',
`RELATED_RECORD` char(20) comment '相关业务记录',
`SEQ` int(11) default '0' comment '排序 (小到大)',
`MODIFIED_ON` timestamp not null default current_timestamp comment '修改时间',
`MODIFIED_BY` char(20) not null comment '修改人',
@ -653,7 +654,8 @@ create table if not exists `project_task` (
`CREATED_ON` timestamp not null default current_timestamp comment '创建时间',
primary key (`TASK_ID`),
index IX0_project_task (`PROJECT_ID`, `PROJECT_PLAN_ID`, `SEQ`),
index IX1_project_task (`PROJECT_ID`, `TASK_NUMBER`, `TASK_NAME`, `STATUS`)
index IX1_project_task (`PROJECT_ID`, `TASK_NUMBER`, `TASK_NAME`, `STATUS`),
index IX2_project_task (`RELATED_RECORD`, `PROJECT_ID`)
)Engine=InnoDB;
-- ************ Entity [ProjectTaskRelation] DDL ************
@ -765,4 +767,4 @@ insert into `classification` (`DATA_ID`, `NAME`, `DESCRIPTION`, `OPEN_LEVEL`, `I
-- DB Version (see `db-upgrade.sql`)
insert into `system_config` (`CONFIG_ID`, `ITEM`, `VALUE`)
values ('021-9000000000000001', 'DBVer', 34);
values ('021-9000000000000001', 'DBVer', 35);

View file

@ -1,6 +1,11 @@
-- Database upgrade scripts for rebuild 1.x and 2.x
-- Each upgraded starts with `-- #VERSION`
-- #35 (v2.3)
alter table `project_task`
add column `RELATED_RECORD` char(20) comment '相关业务记录',
add index IX92_project_task (`RELATED_RECORD`, `PROJECT_ID`);
-- #34 (v2.2)
alter table `revision_history`
add column `IP_ADDR` varchar(100) comment 'IP地址';

View file

@ -127,7 +127,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
position: absolute;
top: 0;
right: 0;
padding-top: 9px;
padding-top: 8px;
padding-left: 6px;
display: none;
text-align: left;

View file

@ -569,19 +569,8 @@ See LICENSE and COMMERCIAL in the project root for license information.
border-top-right-radius: 0;
}
.feed-options.related .related-record {
width: 295px;
}
@media (max-width: 1280px) {
.feed-options.related .related-record {
width: 245px;
}
}
/* in edit */
.modal-body .feed-options.related .related-record {
width: 250px;
.feed-options.related {
padding-right: 16px;
}
.rich-content > .appends {

View file

@ -24,7 +24,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
position: absolute;
top: 0;
right: 0;
padding-top: 9px;
padding-top: 8px;
padding-left: 6px;
display: none;
text-align: left;
@ -62,12 +62,12 @@ See LICENSE and COMMERCIAL in the project root for license information.
}
#plan-boxes .plan-box-wrapper {
flex: 0 0 300px;
flex: 0 0 320px;
margin: 0;
}
#plan-boxes .plan-box-wrapper .plan-box {
width: 275px;
width: 295px;
}
.plan-box .plan-header .plan-title {

View file

@ -2170,6 +2170,14 @@ form.simple {
font-size: 18px !important;
}
.fs-19 {
font-size: 19px !important;
}
.fs-20 {
font-size: 20px !important;
}
.danger-hover:hover {
color: #ea4335 !important;
}

View file

@ -212,7 +212,16 @@ class FeedsEditor extends React.Component {
</div>
</div>
{this.state.type === 4 && <ScheduleOptions ref={(c) => (this._scheduleOptions = c)} initValue={this.state.contentMore} contentMore={this.state.contentMore} />}
{(this.state.type === 2 || this.state.type === 4) && <SelectRelated ref={(c) => (this._selectRelated = c)} initValue={this.state.relatedRecord} />}
{(this.state.type === 2 || this.state.type === 4) && (
<div className="feed-options related">
<dl className="row">
<dt className="col-12 col-lg-3 pt-2">{$L('RelatedRecord')}</dt>
<dd className="col-12 col-lg-9">
<AnyRecordSelector ref={(c) => (this._selectRelated = c)} initValue={this.state.relatedRecord} />
</dd>
</dl>
</div>
)}
{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">
@ -408,104 +417,6 @@ class SelectGroup extends React.Component {
}
}
// ~ 选择相关记录
class SelectRelated extends React.Component {
state = { ...this.props }
render() {
return (
<div className="feed-options related">
<dl className="row">
<dt className="col-12 col-lg-3 pt-2">{$L('ReleatedRecord')}</dt>
<dd className="col-12 col-lg-9">
<span className="float-left" style={{ width: 200 }}>
<select className="form-control form-control-sm" ref={(c) => (this._entity = c)}>
{(this.state.entities || []).length === 0 && <option>{$L('NoAnySome,Entity')}</option>}
{(this.state.entities || []).map((item) => {
return (
<option key={item.name} value={item.name}>
{item.label}
</option>
)
})}
</select>
</span>
<span className="float-left pl-1 related-record">
<select className="form-control form-control-sm float-left" ref={(c) => (this._record = c)} />
</span>
<div className="clearfix"></div>
</dd>
</dl>
</div>
)
}
componentDidMount() {
$.get('/commons/metadata/entities', (res) => {
if (!res.data || res.data.length === 0) {
$(this._entity).attr('disabled', true)
$(this._record).attr('disabled', true)
return
}
this.setState({ entities: res.data }, () => {
$(this._entity)
.select2({
allowClear: false,
})
.on('change', () => {
$(this._record).val(null).trigger('change')
})
// 编辑时
if (this.props.initValue) {
$(this._entity).val(this.props.initValue.entity).trigger('change')
const option = new Option(this.props.initValue.text, this.props.initValue.id, true, true)
$(this._record).append(option)
}
})
})
const that = this
let search_input = null
$(this._record).select2({
placeholder: `${$L('SelectSome,ReleatedRecord')} (${$L('Optional')})`,
minimumInputLength: 0,
maximumSelectionLength: 1,
ajax: {
url: '/commons/search/search',
delay: 300,
data: function (params) {
search_input = params.term
return { entity: $(that._entity).val(), q: params.term }
},
processResults: function (data) {
return { results: data.data }
},
},
language: {
noResults: () => {
return (search_input || '').length > 0 ? $L('NoResults') : $L('InputForSearch')
},
inputTooShort: () => {
return $L('InputForSearch')
},
searching: () => {
return $L('Searching')
},
maximumSelected: () => {
return $L('OnlyXSelected').replace('%d', 1)
},
},
})
}
val() {
return $(this._record).val()
}
reset = () => $(this._record).val(null).trigger('change')
}
// 公告选项
class AnnouncementOptions extends React.Component {
state = { ...this.props }

View file

@ -112,6 +112,14 @@ class TaskForm extends React.Component {
<ValueAttachments attachments={this.state.attachments} $$$parent={this} />
</div>
</div>
<div className="form-group row">
<label className="col-12 col-sm-3 col-form-label">
<i className="icon zmdi zmdi-link zmdi-hc-rotate-45 fs-19" style={{ marginTop: '0.15rem' }} /> {$L('RelatedRecord')}
</label>
<div className="col-12 col-sm-9">
{this.state.projectId && <ValueRelatedRecord relatedRecord={this.state.relatedRecord} relatedRecordData={this.state.relatedRecordData} $$$parent={this} />}
</div>
</div>
<TaskCommentsList taskid={this.props.id} ref={(c) => (this._TaskCommentsList = c)} editable={this.props.editable} />
</div>
)
@ -390,7 +398,7 @@ class ValueDescription extends ValueComp {
if (this.state.editMode) {
return (
<div className="form-control-plaintext">
<textarea value={this.state.description || ''} ref={(c) => (this._editor = c)} />
<textarea defaultValue={this.state.description || ''} ref={(c) => (this._editor = c)} />
<input type="file" className="hide" ref={(c) => (this._fieldValue__upload = c)} />
<div className="mt-2 text-right">
<button onClick={() => this._handleEditMode(false)} className="btn btn-sm btn-link mr-1">
@ -524,7 +532,7 @@ class ValueAttachments extends ValueComp {
<React.Fragment>
<div className="form-control-plaintext">
<input type="file" className="inputfile" id="attachments" ref={(c) => (this._attachments = c)} />
<label htmlFor="attachments" style={{ padding: 0, border: 0, lineHeight: 1 }}>
<label htmlFor="attachments" style={{ padding: 0, border: 0, lineHeight: 1, marginBottom: 0 }}>
<a className="tag-value upload hover">+ {$L('Upload')}</a>
</label>
</div>
@ -548,7 +556,7 @@ class ValueAttachments extends ValueComp {
<i className="file-icon" data-type={$fileExtName(fileName)} />
<span>{fileName}</span>
{del && (
<b title={$L('Remove')} onClick={(e) => this._deleteAttachment(item, e)}>
<b title={$L('Delete')} onClick={(e) => this._deleteAttachment(item, e)}>
<span className="zmdi zmdi-close"></span>
</b>
)}
@ -607,10 +615,11 @@ class ValueTags extends ValueComp {
}
_render(editable) {
const tags = this.state.tags || []
return (
<div className="form-control-plaintext task-tags">
<React.Fragment>
{(this.state.tags || []).map((item) => {
{tags.map((item) => {
const colorStyle = { color: item.color, borderColor: item.color }
return (
<span className="tag-value" key={item.rid} style={colorStyle}>
@ -624,7 +633,7 @@ class ValueTags extends ValueComp {
)
})}
</React.Fragment>
{editable && (
{editable ? (
<span className="dropdown" ref={(c) => (this._dropdown = c)}>
<a className="tag-add" title={$L('ClickAdd')} data-toggle="dropdown">
<i className="zmdi zmdi-plus"></i>
@ -633,6 +642,8 @@ class ValueTags extends ValueComp {
{<ValueTagsEditor ref={(c) => (this._ValueTagsEditor = c)} projectId={this.props.projectId} taskid={this.props.taskid} $$$parent={this} />}
</div>
</span>
) : (
tags.length === 0 && <div className="form-control-plaintext text-muted">{$L('Null')}</div>
)}
</div>
)
@ -640,7 +651,6 @@ class ValueTags extends ValueComp {
componentDidMount() {
const that = this
$unhideDropdown(this._dropdown).on({
'hiden.bs.dropdown': function () {
that._ValueTagsEditor.toggleEditMode(false)
@ -853,6 +863,59 @@ class ValueTagsEditor extends React.Component {
}
}
// 关联记录
class ValueRelatedRecord extends ValueComp {
state = { ...this.props }
renderElement() {
if (this.state.editMode) {
return (
<div className="row">
<div className="col-9">
<AnyRecordSelector initValue={this.state.relatedRecordData} ref={(c) => (this._relatedRecord = c)} />
</div>
<div className="col-3" style={{ paddingTop: '0.18rem' }}>
<button onClick={() => this.setState({ editMode: false })} className="btn btn-sm btn-link mr-1">
{$L('Cancel')}
</button>
<button className="btn btn-sm btn-primary" onClick={() => this.handleChange()}>
{$L('Confirm')}
</button>
</div>
</div>
)
} else {
return this.renderViewElement(true)
}
}
renderViewElement(useEdit) {
const data = this.state.relatedRecordData
if (data) {
return (
<div className={`form-control-plaintext ${useEdit ? 'hover' : ''}`} onClick={() => useEdit && this.setState({ editMode: true })}>
<a href={`${rb.baseUrl}/app/list-and-view?id=${data.id}`} title={$L('ClickViewReleated')} target="_blank" onClick={(e) => $stopEvent(e)}>
{data.text}
</a>
</div>
)
} else {
return (
<div className={`form-control-plaintext text-muted ${useEdit ? 'hover' : ''}`} onClick={() => useEdit && this.setState({ editMode: true })}>
{$L('Null')}
</div>
)
}
}
handleChange() {
const value = this._relatedRecord.value() || null
super.handleChange({ target: { name: 'relatedRecord', value: value ? value.id : null } }, () => {
this.setState({ editMode: false, relatedRecordData: value })
})
}
}
// --
// 评论列表
@ -864,7 +927,7 @@ class TaskCommentsList extends React.Component {
return (
<div className="comment-list-wrap">
<h4>
<i className="zmdi zmdi-comments label-icon down-2"></i> {$L('SomeList,Comment')}
<i className="zmdi zmdi-comments label-icon down-2"></i> {$L('SomeList,Comment')} ({this.state.comments.length})
</h4>
<div className="feeds-list comment-list">
{this.state.comments.map((item) => {

View file

@ -695,6 +695,119 @@ const DateShow = function (props) {
return props.date ? <span title={props.date}>{$fromNow(props.date)}</span> : null
}
// ~~ 任意记录选择
class AnyRecordSelector extends React.Component {
state = { ...this.props }
render() {
return (
<div className="row">
<div className="col-4 pr-0">
<select className="form-control form-control-sm" ref={(c) => (this._entity = c)}>
{(this.state.entities || []).map((item) => {
return (
<option key={item.name} value={item.name}>
{item.label}
</option>
)
})}
</select>
</div>
<div className="col-8 pl-2">
<select className="form-control form-control-sm float-left" ref={(c) => (this._record = c)} />
</div>
</div>
)
}
componentDidMount() {
$.get('/commons/metadata/entities', (res) => {
if ((res.data || []).length === 0) $(this._record).attr('disabled', true)
this.setState({ entities: res.data || [] }, () => {
$(this._entity)
.select2({
placeholder: $L('NoAnySome,Entity'),
allowClear: false,
})
.on('change', () => {
$(this._record).val(null).trigger('change')
})
// 编辑时
const iv = this.props.initValue
if (iv) {
$(this._entity).val(iv.entity).trigger('change')
const option = new Option(iv.text, iv.id, true, true)
$(this._record).append(option)
}
})
})
const that = this
let search_input = null
$(this._record)
.select2({
placeholder: `${$L('SelectSome,Record')}`,
minimumInputLength: 0,
maximumSelectionLength: 1,
ajax: {
url: '/commons/search/search',
delay: 300,
data: function (params) {
search_input = params.term
return {
entity: $(that._entity).val(),
q: params.term,
}
},
processResults: function (data) {
return {
results: data.data,
}
},
},
language: {
noResults: () => {
return (search_input || '').length > 0 ? $L('NoResults') : $L('InputForSearch')
},
inputTooShort: () => {
return $L('InputForSearch')
},
searching: () => {
return $L('Searching')
},
maximumSelected: () => {
return $L('OnlyXSelected').replace('%d', 1)
},
},
})
.on('change', (e) => {
typeof that.props.onSelect === 'function' && that.props.onSelect(e.target.value)
})
}
// return `id`
val() {
return $(this._record).val()
}
// return `{ id:xx, text:xx, entity:xx }`
value() {
const val = this.val()
if (!val) return null
return {
entity: $(this._entity).val(),
id: val,
text: $(this._record).select2('data')[0].text,
}
}
reset() {
$(this._record).val(null).trigger('change')
}
}
// ~~ 默认 SimpleMDE 工具栏
const DEFAULT_MDE_TOOLBAR = [
{