Form area and break 52 (#554)

* form line collapsed and breaked

* better AdvControl

* view approval form

* view form readonly

* tag init
This commit is contained in:
RB 2022-12-12 17:19:53 +08:00 committed by GitHub
parent c95de134c6
commit 7e53852ea9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 232 additions and 96 deletions

View file

@ -112,6 +112,7 @@ public class FormsBuilder extends FormsManager {
final Entity hasMainEntity = entityMeta.getMainEntity();
// 审批流程状态
ApprovalState approvalState;
String readonlyMessage = null;
// 判断表单权限
@ -123,11 +124,10 @@ public class FormsBuilder extends FormsManager {
approvalState = EntityHelper.isUnsavedId(mainid) ? null : getHadApproval(hasMainEntity, mainid);
if ((approvalState == ApprovalState.PROCESSING || approvalState == ApprovalState.APPROVED)) {
return formatModelError(approvalState == ApprovalState.APPROVED
readonlyMessage = approvalState == ApprovalState.APPROVED
? Language.L("主记录已完成审批,不能添加明细")
: Language.L("主记录正在审批中,不能添加明细"));
: Language.L("主记录正在审批中,不能添加明细");
}
// 明细无需审批
approvalState = null;
@ -161,9 +161,9 @@ public class FormsBuilder extends FormsManager {
if (approvalState != null) {
String recordType = hasMainEntity == null ? Language.L("记录") : Language.L("主记录");
if (approvalState == ApprovalState.APPROVED) {
return formatModelError(Language.L("%s已完成审批禁止编辑", recordType));
readonlyMessage = Language.L("%s已完成审批禁止编辑", recordType);
} else if (approvalState == ApprovalState.PROCESSING) {
return formatModelError(Language.L("%s正在审批中禁止编辑", recordType));
readonlyMessage = Language.L("%s正在审批中禁止编辑", recordType);
}
}
}
@ -187,7 +187,7 @@ public class FormsBuilder extends FormsManager {
Set<String> roAutosWithout = record == null ? null : Collections.emptySet();
for (Object o : elements) {
JSONObject field = (JSONObject) o;
if (roAutos.contains(field.getString("field"))) {
if (roAutos.contains(field.getString("field")) || readonlyMessage != null) {
field.put("readonly", true);
// 前端可收集值
@ -213,8 +213,7 @@ public class FormsBuilder extends FormsManager {
// v3.1
if (!entityMeta.getExtraAttrs().getBooleanValue(EasyEntityConfigProps.NOT_COEDITING)) {
model.set("detailMeta", EasyMetaFactory.toJSON(entityMeta.getDetailEntity()));
model.set("detailsNotEmpty",
entityMeta.getExtraAttrs().getBooleanValue(EasyEntityConfigProps.DETAILS_NOTEMPTY));
model.set("detailsNotEmpty", entityMeta.getExtraAttrs().getBooleanValue(EasyEntityConfigProps.DETAILS_NOTEMPTY));
}
}
@ -222,9 +221,8 @@ public class FormsBuilder extends FormsManager {
model.set("lastModified", recordData.getDate(EntityHelper.ModifiedOn).getTime());
}
if (approvalState != null) {
model.set("hadApproval", approvalState.getState());
}
if (approvalState != null) model.set("hadApproval", approvalState.getState());
if (readonlyMessage != null) model.set("readonlyMessage", readonlyMessage);
model.set("id", null); // Clean form's ID of config
return model.toJSON();

View file

@ -41,6 +41,7 @@ public enum DisplayType {
N2NREFERENCE(EasyN2NReference.class, "多引用", FieldType.REFERENCE_LIST, -1, null),
LOCATION(EasyLocation.class, "位置", FieldType.STRING, 100, null),
SIGN(EasySign.class, "签名", FieldType.TEXT, 32767, null, false, true),
TAG(EasyTag.class, "标签", FieldType.REFERENCE_LIST, -1, null),
// 内部

View file

@ -0,0 +1,34 @@
/*!
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.metadata.easymeta;
import cn.devezhao.persist4j.Field;
import com.rebuild.core.support.general.N2NReferenceSupport;
/**
* @author Zixin
* @since 2022/12/12
* @see N2NReferenceSupport
*/
public class EasyTag extends EasyField {
private static final long serialVersionUID = -5827184319679918289L;
protected EasyTag(Field field, DisplayType displayType) {
super(field, displayType);
}
@Override
public Object convertCompatibleValue(Object value, EasyField targetField) {
return super.convertCompatibleValue(value, targetField);
}
@Override
public Object exprDefaultValue() {
return super.exprDefaultValue();
}
}

View file

@ -11,6 +11,10 @@ See LICENSE and COMMERCIAL in the project root for license information.
.rb-right-sidebar.field-aside {
width: 280px;
}
.page-aside,
.field-aside {
z-index: 1;
}

View file

@ -923,7 +923,8 @@ select.form-control:not([disabled]) {
position: relative;
}
.form-layout .form-line {
.form-layout .form-line,
.form-layout .form-line-breaked {
width: 100%;
margin-left: 15px;
margin-right: 15px;
@ -946,9 +947,10 @@ select.form-control:not([disabled]) {
.rbform-alert {
margin: -20px -20px 20px;
text-align: center;
padding: 0.6rem;
padding: 0.4rem;
border-radius: 0;
color: #fff;
color: #836203;
font-weight: normal;
}
@ -2007,7 +2009,7 @@ th.column-fixed {
left: 10px;
margin: -11px 0 0;
padding: 3px 5px 3px 5px;
color: #777;
color: #666;
}
.form-line.hover fieldset legend {
@ -2018,6 +2020,12 @@ th.column-fixed {
color: #4285f4;
}
.form-line-breaked {
height: 0;
font-size: 0;
overflow: hidden;
}
.btn-primary.btn-outline,
.btn-primary.btn-outline[disabled] {
background-color: #fff;
@ -4342,7 +4350,7 @@ html.external-auth .auth-body.must-center .login {
box-shadow: none;
}
.btn.btn-light:hover {
.btn.btn-light:not(:disabled):hover {
background-color: #e6e6e6;
}

View file

@ -280,7 +280,10 @@ const _handlePicklist = function (dt) {
})
if (res.data.length > 5) $('#picklist-items').parent().removeClass('autoh')
})
$('.J_picklist-edit').on('click', () => RbModal.create(`/p/admin/metadata/picklist-editor?entity=${wpc.entityName}&field=${wpc.fieldName}&multi=${dt === 'MULTISELECT'}`, $L('选项配置')))
$('.J_picklist-edit').on('click', () => {
RbModal.create(`/p/admin/metadata/picklist-editor?entity=${wpc.entityName}&field=${wpc.fieldName}&multi=${dt === 'MULTISELECT'}`, $L('配置选项'))
})
}
const _handleSeries = function () {

View file

@ -22,10 +22,11 @@ const FIELD_TYPES = {
'PICKLIST': [$L('下拉列表'), 'mdi-form-select'],
'CLASSIFICATION': [$L('分类'), 'mdi-form-dropdown'],
'MULTISELECT': [$L('多选'), 'mdi-format-list-checks'],
'TAG': [$L('标签'), 'mdi-tag-outline'],
'REFERENCE': [$L('引用'), 'mdi-feature-search-outline'],
'N2NREFERENCE': [$L('多引用'), 'mdi-text-box-search-outline'],
'FILE': [$L('附件'), 'mdi-attachment'],
'IMAGE': [$L('图片'), 'mdi-image'],
'IMAGE': [$L('图片'), 'mdi-image-outline'],
'AVATAR': [$L('头像'), 'mdi-account-box-outline'],
'BARCODE': [$L('二维码'), 'mdi-qrcode'],
'LOCATION': [$L('位置'), 'mdi-map-marker'],

View file

@ -16,7 +16,7 @@ const COLSPANS = {
9: 'w-33',
}
$(document).ready(function () {
$(document).ready(() => {
$.get(`../list-field?entity=${wpc.entityName}`, function (res) {
const validFields = {},
configFields = []
@ -24,26 +24,17 @@ $(document).ready(function () {
configFields.push(this.field)
})
const $advControls = $('#adv-control tbody')
const template = $advControls.find('tr').html()
$advControls.find('tr').remove()
$(res.data).each(function () {
validFields[this.fieldName] = this
if (configFields.includes(this.fieldName) === false) render_unset(this)
// Adv control
const $control = $(`<tr data-field="${this.fieldName}">${template}</tr>`).appendTo($advControls)
$control.find('td:eq(0)').text(this.fieldLabel)
const $req = $control.find('td:eq(2)')
if (this.builtin) $req.empty()
else if (!this.nullable) $req.find('input').attr({ disabled: true, checked: true })
if (!configFields.includes(this.fieldName)) render_unset(this)
})
AdvControl.init()
$(wpc.formConfig.elements).each(function () {
const field = validFields[this.field]
if (this.field === DIVIDER_LINE) {
render_item({ fieldName: this.field, fieldLabel: this.label || '', colspan: 4 })
render_item({ fieldName: this.field, fieldLabel: this.label || '', colspan: 4, collapsed: this.collapsed, breaked: this.breaked })
} else if (!field) {
const $item = $(`<div class="dd-item"><div class="dd-handle J_field J_missed"><span class="text-danger">[${this.field.toUpperCase()}] ${$L('字段已删除')}</span></div></div>`).appendTo(
'.form-preview'
@ -53,8 +44,7 @@ $(document).ready(function () {
$item.remove()
})
} else {
render_item({ ...field, isFull: this.isFull || false, colspan: this.colspan, tip: this.tip || null, height: this.height || null })
AdvControl.set(this)
render_item({ ...field, ...this })
}
})
@ -67,7 +57,7 @@ $(document).ready(function () {
.disableSelection()
})
$('.J_add-divider').on('click', function () {
$('.J_add-divider').on('click', () => {
$('.nav-tabs-classic a[href="#form-design"]').tab('show')
render_item({ fieldName: DIVIDER_LINE, fieldLabel: '', colspan: 4 })
})
@ -112,6 +102,8 @@ $(document).ready(function () {
if (item.field === DIVIDER_LINE) {
item.colspan = 4
item.label = $this.find('span').text() || ''
item.collapsed = $isTrue($this.attr('data-collapsed'))
item.breaked = $isTrue($this.attr('data-breaked'))
} else {
item.colspan = 2 // default
if ($this.parent().hasClass('w-100')) item.colspan = 4
@ -126,7 +118,7 @@ $(document).ready(function () {
const height = $this.attr('data-height')
if (height) item.height = height
AdvControl.append(item)
AdvControl.cfgAppend(item)
}
formElements.push(item)
})
@ -184,15 +176,17 @@ const render_item = function (data) {
`<div class="dd-handle J_field" data-field="${data.fieldName}" data-label="${data.fieldLabel}"><span _title="${isDivider ? $L('分栏') : 'FIELD'}">${data.fieldLabel}</span></div>`
).appendTo($item)
if (data.creatable === false) $handle.addClass('readonly')
else if (data.nullable === false) $handle.addClass('not-nullable')
// 填写提示
if (data.tip) $('<i class="J_tip zmdi zmdi-info-outline"></i>').appendTo($handle.find('span')).attr('title', data.tip)
// 高度
if (data.height) $handle.attr('data-height', data.height)
const $action = $('<div class="dd-action"></div>').appendTo($handle)
// 字段
if (data.displayType) {
if (data.creatable === false) $handle.addClass('readonly')
else if (data.nullable === false) $handle.addClass('not-nullable')
// 填写提示
if (data.tip) $('<i class="J_tip zmdi zmdi-info-outline"></i>').appendTo($handle.find('span')).attr('title', data.tip)
// 长文本高度
if (data.height) $handle.attr('data-height', data.height)
$(`<span class="ft">${data.displayType}</span>`).appendTo($item)
$(`<a class="mr-1 colspan" title="${$L('宽度')}" data-toggle="dropdown"><i class="zmdi zmdi-view-column"></i></a>`).appendTo($action)
@ -246,26 +240,37 @@ const render_item = function (data) {
render_unset(data)
$item.remove()
})
AdvControl.set({ ...data })
}
if (isDivider) {
$item.addClass('divider')
const $handle = $item.find('.dd-handle').attr({
'data-collapsed': data.collapsed,
'data-breaked': data.breaked,
})
$(`<a title="${$L('修改')}"><i class="zmdi zmdi-edit"></i></a>`)
.appendTo($action)
.on('click', function () {
.on('click', () => {
const _onConfirm = function (nv) {
$item.find('.dd-handle span').text(nv.dividerName || '')
$handle.attr('data-collapsed', $isTrue(nv.collapsed))
$handle.attr('data-breaked', $isTrue(nv.breaked))
}
const ov = $item.find('.dd-handle span').text()
renderRbcomp(<DlgEditDivider onConfirm={_onConfirm} dividerName={ov || ''} />)
const ov = {
dividerName: $item.find('.dd-handle span').text() || '',
collapsed: $handle.attr('data-collapsed'),
breaked: $handle.attr('data-breaked'),
}
renderRbcomp(<DlgEditDivider onConfirm={_onConfirm} {...ov} />)
})
$(`<a title="${$L('移除')}"><i class="zmdi zmdi-close"></i></a>`)
.appendTo($action)
.on('click', function () {
$item.remove()
})
.on('click', () => $item.remove())
}
}
@ -344,8 +349,8 @@ class DlgEditField extends RbAlert {
}
handleChange = (e) => {
let target = e.target
let s = {}
const target = e.target
const s = {}
s[target.name] = target.type === 'checkbox' ? target.checked : target.value
this.setState(s)
}
@ -369,6 +374,16 @@ class DlgEditDivider extends DlgEditField {
<label>{$L('分栏名称')}</label>
<input type="text" className="form-control form-control-sm" name="dividerName" value={this.state.dividerName || ''} onChange={this.handleChange} placeholder={$L('输入分栏名称')} />
</div>
<div className="form-group">
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline mt-0 mb-0">
<input className="custom-control-input" type="checkbox" defaultChecked={$isTrue(this.props.collapsed)} name="collapsed" onChange={this.handleChange} />
<span className="custom-control-label">{$L('默认收起')}</span>
</label>
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline mt-0 mb-0 bosskey-show">
<input className="custom-control-input" type="checkbox" defaultChecked={$isTrue(this.props.breaked)} name="breaked" onChange={this.handleChange} />
<span className="custom-control-label">{$L('仅用于断行')}</span>
</label>
</div>
<div className="form-group mb-2">
<button type="button" className="btn btn-primary" onClick={this._onConfirm}>
{$L('确定')}
@ -396,22 +411,33 @@ const add2Layout = function (fieldName) {
// 高级控制
const AdvControl = {
$controls: $('#adv-control tbody'),
$tbody: $('#adv-control tbody'),
append: function (item) {
this.$controls.find(`tr[data-field="${item.field}"] input`).each(function () {
init() {
this._template = this.$tbody.find('tr').html()
this.$tbody.find('tr').remove()
},
set: function (field) {
const $c = $(`<tr data-field="${field.fieldName}">${this._template}</tr>`).appendTo(this.$tbody)
$c.find('td:eq(0)').text(field.fieldLabel)
const $req = $c.find('td:eq(2)')
if (field.builtin) $req.empty()
else if (!field.nullable) $req.find('input').attr({ disabled: true, checked: true })
this.$tbody.find(`tr[data-field="${field.fieldName}"] input`).each(function () {
const $this = $(this)
if ($this.prop('disabled')) return
const v = field[$this.attr('name')]
if (v === true || v === false) $this.attr('checked', v)
})
},
cfgAppend: function (item) {
this.$tbody.find(`tr[data-field="${item.field}"] input`).each(function () {
const $this = $(this)
if ($this.prop('disabled')) return
item[$this.attr('name')] = $this.prop('checked')
})
},
set: function (item) {
this.$controls.find(`tr[data-field="${item.field}"] input`).each(function () {
const $this = $(this)
if ($this.prop('disabled')) return
const v = item[$this.attr('name')]
if (v === true || v === false) $this.attr('checked', v)
})
},
}

View file

@ -115,14 +115,14 @@ class RbFormModal extends React.Component {
const formModel = res.data
const FORM = (
<RbForm entity={entity} id={id} rawModel={formModel} $$$parent={this}>
<RbForm entity={entity} id={id} rawModel={formModel} $$$parent={this} readonly={!!formModel.readonlyMessage}>
{formModel.elements.map((item) => {
return detectElement(item, entity)
})}
</RbForm>
)
this.setState({ formComponent: FORM }, () => {
this.setState({ formComponent: FORM, alertMessage: formModel.readonlyMessage || null }, () => {
this.setState({ inLoad: false })
if (window.FrontJS) {
window.FrontJS.Form._trigger('open', [res.data])
@ -244,8 +244,11 @@ class RbForm extends React.Component {
this.isNew = !props.id
this._postBefore = props.postBefore || props.$$$parent.props.postBefore
this._postAfter = props.postAfter || props.$$$parent.props.postAfter
const $$$props = props.$$$parent && props.$$$parent.props ? props.$$$parent.props : {}
this._postBefore = props.postBefore || $$$props.postBefore
this._postAfter = props.postAfter || $$$props.postAfter
this._dividerRefs = []
}
render() {
@ -253,9 +256,13 @@ class RbForm extends React.Component {
<div className="rbform form-layout">
<div className="form row" ref={(c) => (this._form = c)}>
{this.props.children.map((fieldComp) => {
const refid = fieldComp.props.field === TYPE_DIVIDER ? null : `fieldcomp-${fieldComp.props.field}`
return React.cloneElement(fieldComp, { $$$parent: this, ref: refid })
const ref = fieldComp.props.field === TYPE_DIVIDER ? $random('divider-') : `fieldcomp-${fieldComp.props.field}`
if (fieldComp.props.field === TYPE_DIVIDER && fieldComp.props.collapsed) {
this._dividerRefs.push(ref)
}
return React.cloneElement(fieldComp, { $$$parent: this, ref: ref })
})}
{this.renderCustomizedFormArea()}
</div>
@ -265,6 +272,12 @@ class RbForm extends React.Component {
)
}
renderCustomizedFormArea() {
let _FormArea
if (window._CustomizedForms) _FormArea = window._CustomizedForms.useFormArea(this.props.entity, this)
return _FormArea || null
}
renderDetailForm() {
const detailMeta = this.props.rawModel.detailMeta
if (!detailMeta || !window.ProTable) return null
@ -330,7 +343,10 @@ class RbForm extends React.Component {
// 记录转换:预览模式
const previewid = this.props.$$$parent ? this.props.$$$parent.state.previewid : null
const NADD = [5, 10, 20]
if (!_ProTable) {
_ProTable = <ProTable entity={detailMeta} mainid={this.state.id} previewid={previewid} ref={(c) => (this._ProTable = c)} $$$main={this} />
}
return (
<div className="detail-form-table">
<div className="row">
@ -340,6 +356,7 @@ class RbForm extends React.Component {
{detailMeta.entityLabel}
</h5>
</div>
<div className="col text-right">
{detailImports && detailImports.length > 0 && (
<div className="btn-group mr-2">
@ -365,15 +382,15 @@ class RbForm extends React.Component {
)}
<div className="btn-group">
<button className="btn btn-secondary" type="button" onClick={() => _addNew()}>
<button className="btn btn-secondary" type="button" onClick={() => _addNew()} disabled={this.props.readonly}>
<i className="icon x14 zmdi zmdi-playlist-plus mr-1" />
{$L('添加明细')}
</button>
<button className="btn btn-secondary dropdown-toggle w-auto" type="button" data-toggle="dropdown">
<button className="btn btn-secondary dropdown-toggle w-auto" type="button" data-toggle="dropdown" disabled={this.props.readonly}>
<i className="icon zmdi zmdi-chevron-down" />
</button>
<div className="dropdown-menu dropdown-menu-right">
{NADD.map((n) => {
{[5, 10, 20].map((n) => {
return (
<a className="dropdown-item" onClick={() => _addNew(n)} key={`n-${n}`}>
{$L('添加 %d ', n)}
@ -385,17 +402,11 @@ class RbForm extends React.Component {
</div>
</div>
<div className="mt-2">{_ProTable ? _ProTable : <ProTable entity={detailMeta} mainid={this.state.id} previewid={previewid} ref={(c) => (this._ProTable = c)} $$$main={this} />}</div>
<div className="mt-2">{_ProTable}</div>
</div>
)
}
renderCustomizedFormArea() {
let _FormArea
if (window._CustomizedForms) _FormArea = window._CustomizedForms.useFormArea(this.props.entity, this)
return _FormArea || null
}
renderFormAction() {
let moreActions = []
// 添加明细
@ -423,22 +434,24 @@ class RbForm extends React.Component {
return (
<div className="dialog-footer" ref={(c) => (this._$formAction = c)}>
<button className="btn btn-secondary btn-space mr-2" type="button" onClick={() => this.props.$$$parent.hide()}>
<button className="btn btn-secondary btn-space" type="button" onClick={() => this.props.$$$parent.hide()}>
{$L('取消')}
</button>
<div className="btn-group dropup btn-space">
<button className="btn btn-primary" type="button" onClick={() => this.post()}>
{$L('保存')}
</button>
{moreActions.length > 0 && (
<React.Fragment>
<button className="btn btn-primary dropdown-toggle w-auto" type="button" data-toggle="dropdown">
<i className="icon zmdi zmdi-chevron-up" />
</button>
<div className="dropdown-menu dropdown-menu-primary dropdown-menu-right">{moreActions}</div>
</React.Fragment>
)}
</div>
{!this.props.readonly && (
<div className="btn-group dropup btn-space ml-1">
<button className="btn btn-primary" type="button" onClick={() => this.post()}>
{$L('保存')}
</button>
{moreActions.length > 0 && (
<React.Fragment>
<button className="btn btn-primary dropdown-toggle w-auto" type="button" data-toggle="dropdown">
<i className="icon zmdi zmdi-chevron-up" />
</button>
<div className="dropdown-menu dropdown-menu-primary dropdown-menu-right">{moreActions}</div>
</React.Fragment>
)}
</div>
)}
</div>
)
}
@ -463,6 +476,12 @@ class RbForm extends React.Component {
})
}
// v3.2 默认收起
this._dividerRefs.forEach((d) => {
// eslint-disable-next-line react/no-string-refs
this.refs[d].toggle()
})
setTimeout(() => RbForm.renderAfter(this), 0)
}
@ -2415,11 +2434,14 @@ class RbFormDivider extends React.Component {
}
render() {
if (this.props.breaked === true) {
return <div className="form-line-breaked"></div>
}
return (
<div className="form-line hover" ref={(c) => (this._$formLine = c)}>
<fieldset>
{this.props.label && (
<legend onClick={() => this.toggle()} className="text-bold">
<legend onClick={() => this.toggle()} className="text-bold" title={$L('展开/收起')}>
{this.props.label}
</legend>
)}
@ -2540,3 +2562,40 @@ const __addRecentlyUse = function (id) {
$.post(`/commons/search/recently-add?id=${id}`)
}
}
// -- Lite
// eslint-disable-next-line no-unused-vars
class LiteForm extends RbForm {
renderCustomizedFormArea() {
return null
}
renderDetailForm() {
return null
}
renderFormAction() {
return null
}
componentDidMount() {
super.componentDidMount()
// TODO init...
}
buildFormData() {
const s = {}
const data = this.__FormData || {}
for (let k in data) {
const error = data[k].error
if (error) {
RbHighbar.create(error)
return false
}
s[k] = data[k].value
}
s.metadata = { id: this.props.id || '' }
return s
}
}

View file

@ -16,6 +16,8 @@ class ProTable extends React.Component {
constructor(props) {
super(props)
this.state = {}
this._readonly = props.$$$main.props.readonly
}
render() {
@ -69,10 +71,10 @@ class ProTable extends React.Component {
{FORM}
<td className={`col-action ${fixed && 'column-fixed'}`}>
<button className="btn btn-light hide" title={$L('复制')} onClick={() => this.copyLine(key)}>
<button className="btn btn-light hide" title={$L('复制')} onClick={() => this.copyLine(key)} disabled={this._readonly}>
<i className="icon zmdi zmdi-copy fs-14" />
</button>
<button className="btn btn-light" title={$L('移除')} onClick={() => this.removeLine(key)}>
<button className="btn btn-light" title={$L('移除')} onClick={() => this.removeLine(key)} disabled={this._readonly}>
<i className="icon zmdi zmdi-close fs-16 text-bold" />
</button>
</td>