Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
I
integrity_practice_management_new
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
integrity_practice_management_new
Commits
5c566366
Commit
5c566366
authored
Sep 02, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: inherit grid restrictions on record creation
parent
d5192776
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
165 additions
and
6 deletions
+165
-6
SubjectViewContent.jsx
...ages/RecordDetailDrawer/components/SubjectViewContent.jsx
+2
-1
index.jsx
src/pages/RecordDetailDrawer/index.jsx
+3
-1
SubjectWorkbench.jsx
src/pages/integrity_practice_management/SubjectWorkbench.jsx
+56
-3
integrity-practice-management.business-flow.md
...management/integrity-practice-management.business-flow.md
+9
-1
integrityPracticeWorkbenchService.js
..._practice_management/integrityPracticeWorkbenchService.js
+38
-0
subjectView.js
src/utils/subjectView.js
+57
-0
No files found.
src/pages/RecordDetailDrawer/components/SubjectViewContent.jsx
View file @
5c566366
...
...
@@ -59,11 +59,12 @@ export default function SubjectViewContent({
isFieldDisabled
,
minimal
=
false
,
useAuditMaterialCenter
=
false
,
hideTitle
=
false
,
})
{
const
displayFields
=
buildViewColumns
(
view
,
subject
);
const
contentFields
=
minimal
?
(
displayFields
||
[]).
filter
((
field
)
=>
field
.
field
!==
'title'
)
:
displayFields
;
:
(
displayFields
||
[]).
filter
((
field
)
=>
!
hideTitle
||
field
.
field
!==
'title'
)
;
const
fieldVariant
=
'secondary'
;
if
(
!
contentFields
||
contentFields
.
length
===
0
)
{
...
...
src/pages/RecordDetailDrawer/index.jsx
View file @
5c566366
...
...
@@ -63,6 +63,7 @@ const RecordDetailDrawer = (props, ref) => {
deriveFieldValues
,
useAuditMaterialCenter
=
false
,
initialMode
=
'view'
,
shortTitle
=
false
,
}
=
props
;
const
currentUser
=
useContext
(
UserContext
);
...
...
@@ -187,7 +188,7 @@ const RecordDetailDrawer = (props, ref) => {
);
const
activeView
=
detailViews
.
find
((
view
)
=>
view
.
name
===
activeTabKey
)
||
detailViews
[
0
];
const
hideRecordTitle
=
shouldHideViewTitleColumn
(
activeView
);
const
hideRecordTitle
=
sho
rtTitle
||
sho
uldHideViewTitleColumn
(
activeView
);
const
isRecordLocked
=
remoteRecord
?.
metadata
?.
is_locked
===
true
;
const
effectiveReadonly
=
readonly
||
isRecordLocked
;
const
disabledFieldSet
=
useMemo
(
...
...
@@ -488,6 +489,7 @@ const RecordDetailDrawer = (props, ref) => {
readonly=
{
effectiveReadonly
}
isFieldDisabled=
{
isDrawerFieldDisabled
}
minimal=
{
minimalContent
}
hideTitle=
{
shortTitle
}
useAuditMaterialCenter=
{
useAuditMaterialCenter
}
/>
</
div
>
...
...
src/pages/integrity_practice_management/SubjectWorkbench.jsx
View file @
5c566366
...
...
@@ -2,10 +2,11 @@ import RecordWorkbench from '@/components/record-workbench';
import
RecordDetailDrawer
from
'@/pages/RecordDetailDrawer'
;
import
{
Archive
,
Delete
,
Download
,
Edit
,
Plus
,
Reload
,
Send
}
from
'@/components/AppIcons'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
AlertDialog
,
Button
,
Toast
,
toast
}
from
'@heroui/react'
;
import
{
AlertDialog
,
Button
,
Input
,
Modal
,
TextArea
,
Toast
,
toast
}
from
'@heroui/react'
;
import
{
useCallback
,
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useLocation
}
from
'umi'
;
import
UserAssignmentDialog
from
'./UserAssignmentDialog'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
import
{
getSubjectNameFromPath
,
INTEGRITY_PRACTICE_SUBJECT_CONFIG
,
...
...
@@ -19,7 +20,11 @@ import {
isConfiguredActionVisible
,
loadSubjectRecords
,
loadSubjectResources
,
buildIntegrityCreateFormFields
,
buildIntegrityCreatePayload
,
}
from
'./integrityPracticeWorkbenchService'
;
import
{
createRecord
}
from
'@/services/DataService'
;
import
{
getViewRestrictionDefaults
,
getSelectOptions
,
emptyValueForField
}
from
'@/utils/subjectView'
;
const
EMPTY_ARRAY
=
[];
const
MUTATING_ACTION_PATTERN
=
/delete|archive|create|push/i
;
...
...
@@ -89,6 +94,9 @@ export default function SubjectWorkbench() {
const
[
refreshKey
,
setRefreshKey
]
=
useState
(
0
);
const
[
assignmentState
,
setAssignmentState
]
=
useState
(
null
);
const
[
confirmationState
,
setConfirmationState
]
=
useState
(
null
);
const
[
showCreate
,
setShowCreate
]
=
useState
(
false
);
const
[
createValues
,
setCreateValues
]
=
useState
({});
const
[
createError
,
setCreateError
]
=
useState
(
''
);
const
activeView
=
useMemo
(
()
=>
views
.
find
((
view
)
=>
view
.
name
===
activeViewName
)
||
views
[
0
]
||
null
,
...
...
@@ -101,6 +109,9 @@ export default function SubjectWorkbench() {
);
const
filterSchema
=
useMemo
(()
=>
buildSubjectFilterSchema
(
activeView
,
subject
),
[
activeView
,
subject
]);
const
createFields
=
useMemo
(()
=>
buildIntegrityCreateFormFields
(
activeView
,
subject
),
[
activeView
,
subject
]);
const
restrictionDefaults
=
useMemo
(()
=>
getViewRestrictionDefaults
(
activeView
,
subject
),
[
activeView
,
subject
]);
const
hideCreateTitle
=
activeView
?.
settings
?.
shortTitle
===
true
;
useEffect
(()
=>
{
let
cancelled
=
false
;
...
...
@@ -164,6 +175,21 @@ export default function SubjectWorkbench() {
};
},
[
subjectName
,
activeView
,
page
,
pageSize
,
filters
,
columns
,
paginationEnabled
,
refreshKey
]);
useEffect
(()
=>
{
if
(
!
showCreate
)
setCreateValues
(
createFields
.
reduce
((
values
,
field
)
=>
({
...
values
,
[
field
.
fieldDef
.
name
]:
restrictionDefaults
.
metadata
?.[
field
.
fieldDef
.
name
]
??
emptyValueForField
(
field
.
fieldDef
)
}),
{}));
},
[
createFields
,
restrictionDefaults
,
showCreate
]);
const
handleCreate
=
async
()
=>
{
if
(
restrictionDefaults
.
errors
.
length
)
{
setCreateError
(
restrictionDefaults
.
errors
.
join
(
'
\
n'
));
return
;
}
if
(
!
hideCreateTitle
&&
!
String
(
createValues
.
title
||
''
).
trim
())
{
setCreateError
(
'记录标题为必填项。'
);
return
;
}
setActionLoading
(
true
);
setCreateError
(
''
);
try
{
await
createRecord
(
buildIntegrityCreatePayload
(
subjectName
,
createValues
,
createFields
,
restrictionDefaults
,
{
hideTitle
:
hideCreateTitle
}));
setShowCreate
(
false
);
setRefreshKey
((
value
)
=>
value
+
1
);
}
catch
(
error
)
{
setCreateError
(
error
?.
message
||
'新增记录失败,请稍后重试。'
);
}
finally
{
setActionLoading
(
false
);
}
};
const
runAction
=
useCallback
(
async
(
actionName
,
context
,
assignment
=
null
)
=>
{
setActionLoading
(
true
);
...
...
@@ -243,14 +269,17 @@ export default function SubjectWorkbench() {
const
toolbarActions
=
useMemo
(
()
=>
(
gridConfig
.
tabBarRightSlot
||
[]).
map
((
action
,
index
)
=>
({
[
...(
gridConfig
.
readonly
?.
disableAddRow
?
[]
:
[{
key
:
'create'
,
label
:
'新增记录'
,
tone
:
'primary'
,
disabled
:
actionLoading
,
onClick
:
()
=>
{
setCreateError
(
''
);
setShowCreate
(
true
);
}
}]),
...(
gridConfig
.
tabBarRightSlot
||
[]).
map
((
action
,
index
)
=>
({
key
:
action
.
execute
||
`toolbar-
${
index
}
`
,
label
:
action
.
label
,
tone
:
'primary'
,
disabled
:
actionLoading
,
onClick
:
(
context
)
=>
requestAction
(
action
,
context
),
})),
[
actionLoading
,
gridConfig
.
tabBarRightSlot
,
requestAction
],
],
[
actionLoading
,
gridConfig
.
readonly
,
gridConfig
.
tabBarRightSlot
,
requestAction
],
);
const
selectionActions
=
useMemo
(
...
...
@@ -325,6 +354,7 @@ export default function SubjectWorkbench() {
onDataChange=
{
onDataChange
}
filterView=
{
subjectConfig
?.
drawerConfig
?.
filterView
||
EMPTY_ARRAY
}
authorizationView=
{
activeView
}
shortTitle=
{
hideCreateTitle
}
useAuditMaterialCenter
/>
),
...
...
@@ -351,6 +381,29 @@ export default function SubjectWorkbench() {
}
}
/>
<
Modal
.
Root
>
<
Modal
.
Trigger
aria
-
hidden=
"true"
className=
"sr-only"
tabIndex=
{
-
1
}
/>
<
Modal
.
Backdrop
isOpen=
{
showCreate
}
onOpenChange=
{
(
open
)
=>
{
if
(
!
open
&&
!
actionLoading
)
setShowCreate
(
false
);
}
}
>
<
Modal
.
Container
size=
"lg"
><
Modal
.
Dialog
>
<
Modal
.
Header
><
Modal
.
Heading
>
新增记录
</
Modal
.
Heading
></
Modal
.
Header
>
<
Modal
.
Body
>
{
createError
?
<
AlertDialog
.
Body
><
div
className=
"whitespace-pre-line text-danger"
>
{
createError
}
</
div
></
AlertDialog
.
Body
>
:
null
}
<
div
className=
"grid grid-cols-1 gap-4 md:grid-cols-2"
>
{
createFields
.
map
((
field
)
=>
{
const
name
=
field
.
fieldDef
.
name
;
const
value
=
createValues
[
name
]
??
''
;
const
options
=
getSelectOptions
(
field
.
fieldDef
);
return
<
div
key=
{
field
.
field
}
className=
"min-w-0"
>
<
label
className=
"mb-1.5 block text-sm font-medium"
>
{
field
.
label
||
field
.
fieldDef
.
label
||
name
}{
field
.
fieldDef
.
required
?
' *'
:
''
}
</
label
>
{
options
.
length
?
<
XinyuanAutocomplete
aria
-
label=
{
field
.
label
||
name
}
options=
{
options
.
map
((
option
)
=>
({
...
option
,
id
:
String
(
option
.
value
)
}))
}
selectedKey=
{
value
||
null
}
valueText=
{
options
.
find
((
option
)
=>
option
.
value
===
value
)?.
label
||
'请选择'
}
isDisabled=
{
Object
.
prototype
.
hasOwnProperty
.
call
(
restrictionDefaults
.
metadata
,
name
)
}
onSelectionChange=
{
(
key
)
=>
setCreateValues
((
current
)
=>
({
...
current
,
[
name
]:
key
?
String
(
key
)
:
''
}))
}
/>
:
field
.
fieldDef
.
type
===
'long_text'
?
<
TextArea
value=
{
value
||
''
}
onChange=
{
(
event
)
=>
setCreateValues
((
current
)
=>
({
...
current
,
[
name
]:
event
.
target
.
value
}))
}
/>
:
<
Input
type=
{
field
.
fieldDef
.
type
===
'number'
?
'number'
:
field
.
fieldDef
.
type
===
'date'
?
'date'
:
'text'
}
value=
{
value
||
''
}
isDisabled=
{
Object
.
prototype
.
hasOwnProperty
.
call
(
restrictionDefaults
.
metadata
,
name
)
}
onChange=
{
(
event
)
=>
setCreateValues
((
current
)
=>
({
...
current
,
[
name
]:
event
.
target
.
value
}))
}
/>
}
</
div
>;
})
}
</
div
>
</
Modal
.
Body
>
<
Modal
.
Footer
><
Button
variant=
"secondary"
onPress=
{
()
=>
setShowCreate
(
false
)
}
>
取消
</
Button
><
Button
isPending=
{
actionLoading
}
onPress=
{
handleCreate
}
>
确认新增
</
Button
></
Modal
.
Footer
>
</
Modal
.
Dialog
></
Modal
.
Container
>
</
Modal
.
Backdrop
>
</
Modal
.
Root
>
<
ActionConfirmationDialog
state=
{
confirmationState
}
submitting=
{
actionLoading
}
...
...
src/pages/integrity_practice_management/integrity-practice-management.business-flow.md
View file @
5c566366
...
...
@@ -27,7 +27,7 @@
4.
`gridRecordConfig.pagination = false`
时单次最多读取 1000 条;其余页面使用服务端分页。
5.
列表不再提供独立关键词搜索;用户通过当前视图字段筛选,筛选条件直接写入
`/record/query`
的
`filter`
,清空筛选后回到第 1 页。
6.
列定义、列顺序、初始排序、固定列和字段显示格式由 Subject View 与 Subject 字段定义共同决定;HeroUI Pro DataGrid 仅在真实横向溢出时固定首列和操作列,并允许当前页原生列排序和列宽调整,不改变服务端分页口径。
`settings.shortTitle === true`
时
只隐藏 title 展示列,标题仍作为记录身份和保存字段;false 或未配置时正常展示。若 View 配置包含标题列,标题也会作为可筛选字段显示
。
`settings.shortTitle === true`
时
隐藏 title 展示列,且新建/编辑弹窗不显示、不校验、不提交根级
`title`
;false 或未配置时保留标题列及标题必填逻辑
。
7.
DataGrid 使用原生 Checkbox 选择;选中后显示 HeroUI Pro ActionBar。行操作 1-3 项直接展示,4 项以上展示前两项与“更多”菜单,删除等危险动作始终排在最后。
8.
点击列表操作列的预览图标打开 HeroUI Modal 并进入只读查看态;首个业务列只展示记录文本,不再承担预览交互。点击弹窗统一的“编辑”按钮或列表操作列铅笔图标进入整体编辑态,保存后返回查看态。字段权限、锁定规则和 Subject 字段类型保持不变。
9.
当前 Detail 有有效
`settings.auth`
或
`settings.disableColumns`
时,字段权限整套使用 Detail;两项都未配置或没有实际 Detail 时,整体回退打开详情的当前 Grid。两套权限不合并、不逐字段回退;
`disableColumns`
对管理员也始终只读,普通用户在选定权限来源中缺少字段
`auth`
时只读。切换 Detail 后立即重算字段状态和编辑入口,编辑入口只检查当前 Detail 展示字段。Grid
`viewAuth`
只控制 Tab 可见性,不隐藏详情字段。
...
...
@@ -40,6 +40,12 @@
-
没有可访问
`grid`
视图时不创建兜底 View、不查询记录,并隐藏新增、批量和行操作;只有一个可访问 View 时隐藏 Tabs 轨道。
-
配置动作执行期间禁用重复点击;成功后使用顶部 Toast,失败时保留当前列表并显示服务端错误。
### 2.1 新建记录与 View 限制
-
新建记录默认继承当前 Grid View 的
`settings.restrictions`
。
`$eq`
或单值
`$in`
条件会将唯一值写入
`metadata`
,对应控件禁用;即使字段未列入弹窗,也会随请求写入。
-
多值集合、否定条件或同一字段互相冲突时无法确定唯一值,禁止创建并提示“请先调整视图配置”。
-
新建请求使用
`POST /record`
,所有业务字段放在
`metadata`
;编辑已有记录只提交用户修改字段,不使用 Grid View 限制覆盖原值。
## 3. 记录选择与动作规则
-
行动作来自
`gridRecordConfig.menuConfig`
,顶部动作来自
`gridRecordConfig.tabBarRightSlot`
。
...
...
@@ -71,6 +77,8 @@
4.
为每个填报人调用
`oa_notice:createProgress`
创建 OA 待办,并将任务 ID 写入
`metadata.progress_id_editor`
。
5.
创建并发送站内通知,流程链接继续使用历史兼容入口
`/self_inspection_form?id=:flowName`
。
填写人提交后,在审核人尚未处理前,填写人可在流程页点击“撤回提交”。撤回会将流程主单的
`editor_status`
、
`auditor_status`
重置为“未完成”,并调用
`oa_notice:updateProgressStatus`
将审核人待办标记为已完成;撤回不发送站内通知。审核人已完成审批或驳回后不可撤回。
限制与异常:
-
未选择记录、填报人或审核人时不创建流程。
...
...
src/pages/integrity_practice_management/integrityPracticeWorkbenchService.js
View file @
5c566366
...
...
@@ -24,6 +24,10 @@ import {
checkViewCondition
,
isViewAccessible
,
mergeRestrictionsIntoFilter
,
getViewRestrictionDefaults
,
emptyValueForField
,
normalizeValueForSubmit
,
getFieldNameFromPath
,
normalizeText
,
sortRecordsByView
,
toArray
,
...
...
@@ -35,6 +39,37 @@ import * as XLSX from 'xlsx';
export
const
DEFAULT_PAGE_SIZE
=
10
;
export
function
buildIntegrityCreateFormFields
(
gridView
,
subject
)
{
const
allColumns
=
buildViewColumns
(
gridView
,
subject
,
{
includeHidden
:
true
});
const
includeTitle
=
gridView
?.
settings
?.
shortTitle
!==
true
;
const
fields
=
allColumns
.
filter
((
column
)
=>
!
column
.
hide
&&
column
.
field
?.
startsWith
(
'metadata.'
)
&&
column
.
fieldDef
?.
name
)
.
reduce
((
result
,
column
)
=>
{
if
(
!
result
.
some
((
field
)
=>
field
.
fieldDef
.
name
===
column
.
fieldDef
.
name
))
result
.
push
(
column
);
return
result
;
},
[]);
const
fallback
=
fields
.
length
||
toArray
(
gridView
?.
settings
?.
columns
).
length
?
fields
:
toArray
(
subject
?.
fields
).
map
((
field
)
=>
({
field
:
`metadata.
${
field
.
name
}
`
,
label
:
field
.
label
||
field
.
name
,
fieldDef
:
field
}));
return
includeTitle
?
[{
field
:
'title'
,
label
:
'记录标题'
,
fieldDef
:
{
name
:
'title'
,
type
:
'text'
,
label
:
'记录标题'
,
required
:
true
}
},
...
fallback
]
:
fallback
;
}
export
function
buildIntegrityCreatePayload
(
subjectName
,
formValues
,
fields
,
restrictionDefaults
=
{},
{
hideTitle
=
false
}
=
{})
{
const
metadata
=
fields
.
reduce
((
result
,
field
)
=>
{
const
name
=
field
.
fieldDef
?.
name
;
if
(
!
name
||
name
===
'title'
)
return
result
;
return
{
...
result
,
[
name
]:
normalizeValueForSubmit
(
formValues
[
name
],
field
.
fieldDef
)
};
},
{});
const
resolvedTitle
=
hideTitle
?
undefined
:
normalizeText
(
formValues
.
title
)
||
restrictionDefaults
.
title
||
'新建记录'
;
return
{
subject
:
subjectName
,
...(
resolvedTitle
?
{
title
:
resolvedTitle
}
:
{}),
metadata
:
{
...
metadata
,
...(
restrictionDefaults
.
metadata
||
{})
},
};
}
function
buildViewFilter
(
view
,
filters
=
{})
{
const
restrictions
=
view
?.
settings
?.
restrictions
||
[];
const
filter
=
mergeRestrictionsIntoFilter
({},
restrictions
);
...
...
@@ -522,6 +557,9 @@ async function pushSelfInspectionForm(records, userInfo, assignment) {
sender: userInfo?._id,
editor: fillers,
auditor: auditors,
editor_status: '未完成',
auditor_status: '未完成',
sender_status: '未完成',
restriction_key: '_id',
restriction_value: outcomeIds,
view_name: toArray(views)[0]?.name || '',
...
...
src/utils/subjectView.js
View file @
5c566366
...
...
@@ -450,6 +450,63 @@ export function mergeRestrictionsIntoFilter(baseFilter = {}, restrictions = [])
},
{
...
baseFilter
});
}
function
hasRestrictionValue
(
value
)
{
return
value
!==
undefined
&&
value
!==
null
&&
value
!==
''
;
}
function
restrictionValuesEqual
(
left
,
right
)
{
if
(
Object
.
is
(
left
,
right
))
return
true
;
try
{
return
JSON
.
stringify
(
left
)
===
JSON
.
stringify
(
right
);
}
catch
(
error
)
{
return
false
;
}
}
/** Resolve only restrictions that identify one safe value for a newly created record. */
export
function
getViewRestrictionDefaults
(
view
,
subject
)
{
const
assignments
=
new
Map
();
const
unresolved
=
new
Set
();
const
errors
=
new
Map
();
const
addError
=
(
fieldPath
,
fieldDef
)
=>
{
if
(
errors
.
has
(
fieldPath
))
return
;
const
label
=
fieldDef
?.
label
||
getFieldNameFromPath
(
fieldPath
)
||
fieldPath
;
errors
.
set
(
fieldPath
,
`
${
label
}
的视图约束无法自动确定唯一值,请先调整视图配置。`
);
};
toArray
(
view
?.
settings
?.
restrictions
).
forEach
((
restriction
)
=>
{
if
(
restriction
?.
type
!==
'field'
||
!
restriction
.
field
||
restriction
.
value
===
undefined
)
return
;
const
fieldPath
=
restriction
.
field
===
'title'
||
restriction
.
field
.
startsWith
(
'metadata.'
)
?
restriction
.
field
:
`metadata.
${
restriction
.
field
}
`
;
const
fieldDef
=
getFieldDefinition
(
fieldPath
,
subject
);
const
comparator
=
normalizeText
(
restriction
.
comparator
||
'$in'
);
const
values
=
toArray
(
restriction
.
value
).
filter
(
hasRestrictionValue
);
if
(
!
[
'$eq'
,
'$in'
].
includes
(
comparator
)
||
values
.
length
!==
1
)
{
unresolved
.
add
(
fieldPath
);
addError
(
fieldPath
,
fieldDef
);
return
;
}
if
(
unresolved
.
has
(
fieldPath
))
return
;
let
value
=
normalizeValueForSubmit
(
values
[
0
],
fieldDef
);
if
(
fieldDef
?.
multiple
)
value
=
toArray
(
value
);
if
(
assignments
.
has
(
fieldPath
)
&&
!
restrictionValuesEqual
(
assignments
.
get
(
fieldPath
),
value
))
{
assignments
.
delete
(
fieldPath
);
unresolved
.
add
(
fieldPath
);
addError
(
fieldPath
,
fieldDef
);
return
;
}
assignments
.
set
(
fieldPath
,
value
);
});
const
metadata
=
{};
let
title
;
assignments
.
forEach
((
value
,
fieldPath
)
=>
{
if
(
unresolved
.
has
(
fieldPath
))
return
;
if
(
fieldPath
===
'title'
)
title
=
value
;
else
{
const
fieldName
=
getFieldNameFromPath
(
fieldPath
);
if
(
fieldName
)
metadata
[
fieldName
]
=
value
;
}
});
return
{
metadata
,
title
,
errors
:
[...
errors
.
values
()]
};
}
export
function
sortRecordsByView
(
records
,
view
)
{
const
sortColumns
=
toArray
(
view
?.
settings
?.
columns
)
.
filter
((
column
)
=>
column
.
sort
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment