Commit 87cf7c70 authored by drigle's avatar drigle

feat: sync view filters and reference selection

parent da2b0019
---
name: subject-schema-manager
description: 同步 Stream Subject 对象文档,并在明确确认后创建、更新或删除对象与字段。
---
# Subject Schema Manager
本项目的 Subject 结构以线上 Subject API 为准,`src/services/subjectsDefinition.md` 是版本化快照。
## 文档同步
使用项目内脚本或全局脚本执行同步:
```bash
node /Users/drigle/.codex/skills/subject-schema-manager/scripts/subject_schema_manager.mjs sync \
--project /Users/drigle/Desktop/xy/integrity_practice_management_new --ticket-stdin
```
脚本从源代码发现 Subject 根对象,递归跟踪字段 `settings.subject`,只更新 `subjectsDefinition.md` 的托管区块,不修改线上对象。
## 对象与字段变更
创建、更新或删除对象/字段前,必须先读取 `src/services/record-api.md` 与 `src/services/subjectsDefinition.md`,生成包含对象名、字段别名、payload 和依赖顺序的计划,并获得用户明确确认。确认后使用脚本 `apply --plan-file ... --confirmed` 执行限定操作。
引用字段只保存目标 `_id` 或 `_id[]`;日期使用 ISO 8601;业务字段始终放在 `metadata` 中。文档清理不等同于远程删除,删除必须在确认计划中显式列出。
Subject schema 同步或变更不改变业务流程;若流程发生变化,必须同步更新对应 `*.business-flow.md`。
interface:
display_name: Subject Schema Manager
short_description: 同步对象文档并管理 Subject 对象与字段
default_prompt: 同步当前项目 Subject 定义,或按确认计划创建、更新、删除对象与字段。
...@@ -15,6 +15,16 @@ function normalizeText(value) { ...@@ -15,6 +15,16 @@ function normalizeText(value) {
return String(value || '').trim(); return String(value || '').trim();
} }
function normalizeRecordId(value) {
if (value === null || value === undefined || value === '') return '';
if (typeof value === 'object') {
const candidate = value._id ?? value.id ?? value.$oid ?? value.name;
if (candidate === value) return '';
return normalizeRecordId(candidate);
}
return normalizeText(value);
}
function escapeMongoRegex(value) { function escapeMongoRegex(value) {
return normalizeText(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return normalizeText(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
} }
...@@ -74,19 +84,11 @@ function resolveRecordId(record) { ...@@ -74,19 +84,11 @@ function resolveRecordId(record) {
} }
// Reference 写入协议:只接受 _id / id // Reference 写入协议:只接受 _id / id
return record.id || record._id; return normalizeRecordId(record.id ?? record._id);
} }
function resolveReferenceValueId(value) { function resolveReferenceValueId(value) {
if (!value) { return normalizeRecordId(value);
return '';
}
if (typeof value === 'object') {
return normalizeText(resolveRecordId(value));
}
return normalizeText(value);
} }
function resolveRecordSubjectName(record, fallbackSubject = '') { function resolveRecordSubjectName(record, fallbackSubject = '') {
......
...@@ -20,10 +20,10 @@ function defaultFilter(textValue, inputValue) { ...@@ -20,10 +20,10 @@ function defaultFilter(textValue, inputValue) {
function getSelectedKeySet(selectionMode, selectedKey, selectedKeys) { function getSelectedKeySet(selectionMode, selectedKey, selectedKeys) {
if (selectionMode === 'multiple') { if (selectionMode === 'multiple') {
return new Set(selectedKeys || []); if (selectedKeys === 'all') return new Set();
return new Set(Array.from(selectedKeys || []).map(toSelectionKey).filter(Boolean));
} }
return new Set(selectedKey === null || selectedKey === undefined ? [] : [toSelectionKey(selectedKey)].filter(Boolean));
return new Set(selectedKey === null || selectedKey === undefined ? [] : [selectedKey]);
} }
function toSelectionKey(value) { function toSelectionKey(value) {
...@@ -76,42 +76,20 @@ export default function XinyuanAutocomplete({ ...@@ -76,42 +76,20 @@ export default function XinyuanAutocomplete({
(selectedOptions.length > 0 (selectedOptions.length > 0
? selectedOptions.map((option) => option.label).join('、') ? selectedOptions.map((option) => option.label).join('、')
: placeholder); : placeholder);
const rootSelectionProps = const isMultiple = selectionMode === 'multiple';
selectionMode === 'multiple' const rootSelectionProps = isMultiple
? { selectedKeys: selectedKeyStringSet } ? { selectedKey: null }
: { selectedKey }; : { selectedKey: selectedKeyStringSet.values().next().value || null };
const listBoxSelectionProps = const listBoxSelectionProps = isMultiple
selectionMode === 'multiple' ? { selectionMode: 'single', onAction: (key) => {
? { const optionKey = toSelectionKey(key);
selectionMode, if (!optionKey || !onSelectionChange) return;
selectedKeys: selectedKeyStringSet, const nextKeys = new Set(selectedKeyStringSet);
} if (nextKeys.has(optionKey)) nextKeys.delete(optionKey); else nextKeys.add(optionKey);
: {};
const visibleOptions = isLoading || errorText ? [] : options;
const handleMultipleOptionPress = (option) => {
if (
selectionMode !== 'multiple' ||
!onSelectionChange ||
option?.isDisabled
) {
return;
}
const optionKey = toSelectionKey(option.id);
if (!optionKey) {
return;
}
const nextKeys = new Set(
Array.from(selectedKeyStringSet).filter(Boolean),
);
if (nextKeys.has(optionKey)) {
nextKeys.delete(optionKey);
} else {
nextKeys.add(optionKey);
}
onSelectionChange(nextKeys); onSelectionChange(nextKeys);
}; } }
: { selectionMode: 'single', selectedKeys: selectedKeyStringSet, onSelectionChange };
const visibleOptions = isLoading || errorText ? [] : options.map((option) => ({ ...option, isSelected: selectedKeyStringSet.has(toSelectionKey(option.id)) }));
return ( return (
<Autocomplete <Autocomplete
...@@ -123,7 +101,7 @@ export default function XinyuanAutocomplete({ ...@@ -123,7 +101,7 @@ export default function XinyuanAutocomplete({
isOpen={isOpen} isOpen={isOpen}
isRequired={isRequired} isRequired={isRequired}
name={name} name={name}
selectionMode={selectionMode} selectionMode={isMultiple ? 'single' : selectionMode}
variant={variant} variant={variant}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
...@@ -180,16 +158,14 @@ export default function XinyuanAutocomplete({ ...@@ -180,16 +158,14 @@ export default function XinyuanAutocomplete({
)} )}
> >
{(option) => { {(option) => {
const isSelected = selectedKeyStringSet.has(toSelectionKey(option.id)); const isSelected = Boolean(option.isSelected);
return ( return (
<ListBox.Item <ListBox.Item
id={option.id} id={option.id}
isDisabled={option.isDisabled} isDisabled={option.isDisabled}
isSelected={isSelected}
textValue={option.textValue || option.label} textValue={option.textValue || option.label}
{...(selectionMode === 'multiple'
? { onPress: () => handleMultipleOptionPress(option) }
: {})}
> >
{renderOption ? ( {renderOption ? (
renderOption(option) renderOption(option)
......
...@@ -6,6 +6,7 @@ import { DataGrid } from '@heroui-pro/react/data-grid'; ...@@ -6,6 +6,7 @@ import { DataGrid } from '@heroui-pro/react/data-grid';
import { EmptyState } from '@heroui-pro/react/empty-state'; import { EmptyState } from '@heroui-pro/react/empty-state';
import { Alert, Button, Dropdown, Label, Pagination, SearchField, Spinner, Surface, Tabs, Tooltip, buttonVariants } from '@heroui/react'; import { Alert, Button, Dropdown, Label, Pagination, SearchField, Spinner, Surface, Tabs, Tooltip, buttonVariants } from '@heroui/react';
import { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import XinyuanAutocomplete from '@/components/XinyuanAutocomplete';
const EMPTY_ARRAY = []; const EMPTY_ARRAY = [];
const ACTION_ICON_CLASS = '!grid size-4 shrink-0 place-items-center !leading-none [&>svg]:!m-0 [&>svg]:block [&>svg]:!size-4'; const ACTION_ICON_CLASS = '!grid size-4 shrink-0 place-items-center !leading-none [&>svg]:!m-0 [&>svg]:block [&>svg]:!size-4';
...@@ -313,8 +314,8 @@ export default function RecordWorkbench({ ...@@ -313,8 +314,8 @@ export default function RecordWorkbench({
else onSelectionChange?.(Array.from(selection).map(String)); else onSelectionChange?.(Array.from(selection).map(String));
}; };
const listSearchField = filterSchema.find((field) => field?.type === 'text') || null; const listSearchField = null;
const filterCardSchema = filterSchema.filter((field) => field !== listSearchField && field?.type === 'text'); const filterCardSchema = filterSchema;
const hasViewSwitch = views.length > 1; const hasViewSwitch = views.length > 1;
return ( return (
...@@ -340,11 +341,13 @@ export default function RecordWorkbench({ ...@@ -340,11 +341,13 @@ export default function RecordWorkbench({
<Surface className="p-6"> <Surface className="p-6">
<div className="flex flex-col gap-3 md:flex-row md:items-end"> <div className="flex flex-col gap-3 md:flex-row md:items-end">
<div className="grid min-w-0 flex-1 gap-3 md:grid-cols-3"> <div className="grid min-w-0 flex-1 gap-3 md:grid-cols-3">
{filterCardSchema.map((field) => field.type === 'text' ? ( {filterCardSchema.map((field) => field.type === 'select' ? (
<XinyuanAutocomplete key={field.key} aria-label={field.label} options={field.options || []} selectedKey={filters[field.key] || null} placeholder={field.placeholder} valueText={(field.options || []).find((item) => item.value === filters[field.key])?.label || field.placeholder} onSelectionChange={(key) => onFiltersChange?.(field.key, key ? String(key) : '')} />
) : (
<SearchField aria-label={field.label} fullWidth key={field.key} value={filters[field.key] || ''} onChange={(value) => onFiltersChange?.(field.key, value)}> <SearchField aria-label={field.label} fullWidth key={field.key} value={filters[field.key] || ''} onChange={(value) => onFiltersChange?.(field.key, value)}>
<SearchField.Group><SearchField.SearchIcon><Search size={16} /></SearchField.SearchIcon><SearchField.Input placeholder={field.placeholder} /><SearchField.ClearButton aria-label="清空搜索" /></SearchField.Group> <SearchField.Group><SearchField.SearchIcon><Search size={16} /></SearchField.SearchIcon><SearchField.Input type={field.type === 'date' ? 'date' : 'text'} placeholder={field.placeholder} /><SearchField.ClearButton aria-label="清空搜索" /></SearchField.Group>
</SearchField> </SearchField>
) : null)} ))}
</div> </div>
{onClearFilters ? ( {onClearFilters ? (
<div className="flex shrink-0 items-center"> <div className="flex shrink-0 items-center">
......
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
import { import {
actionNeedsAssignment, actionNeedsAssignment,
buildSubjectColumns, buildSubjectColumns,
buildSubjectFilterSchema,
DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE,
executeConfiguredAction, executeConfiguredAction,
isConfiguredActionVisible, isConfiguredActionVisible,
...@@ -78,8 +79,7 @@ export default function SubjectWorkbench() { ...@@ -78,8 +79,7 @@ export default function SubjectWorkbench() {
const [activeViewName, setActiveViewName] = useState(''); const [activeViewName, setActiveViewName] = useState('');
const [records, setRecords] = useState(EMPTY_ARRAY); const [records, setRecords] = useState(EMPTY_ARRAY);
const [selectedIds, setSelectedIds] = useState(EMPTY_ARRAY); const [selectedIds, setSelectedIds] = useState(EMPTY_ARRAY);
const [keyword, setKeyword] = useState(''); const [filters, setFilters] = useState({});
const [debouncedKeyword, setDebouncedKeyword] = useState('');
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const pageSize = DEFAULT_PAGE_SIZE; const pageSize = DEFAULT_PAGE_SIZE;
const [pagination, setPagination] = useState(null); const [pagination, setPagination] = useState(null);
...@@ -100,10 +100,7 @@ export default function SubjectWorkbench() { ...@@ -100,10 +100,7 @@ export default function SubjectWorkbench() {
[activeView, subject], [activeView, subject],
); );
useEffect(() => { const filterSchema = useMemo(() => buildSubjectFilterSchema(activeView, subject), [activeView, subject]);
const timer = window.setTimeout(() => setDebouncedKeyword(keyword), 250);
return () => window.clearTimeout(timer);
}, [keyword]);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
...@@ -111,7 +108,7 @@ export default function SubjectWorkbench() { ...@@ -111,7 +108,7 @@ export default function SubjectWorkbench() {
setError(''); setError('');
setRecords(EMPTY_ARRAY); setRecords(EMPTY_ARRAY);
setSelectedIds(EMPTY_ARRAY); setSelectedIds(EMPTY_ARRAY);
setKeyword(''); setFilters({});
setPage(0); setPage(0);
loadSubjectResources(subjectName, subjectConfig) loadSubjectResources(subjectName, subjectConfig)
...@@ -144,7 +141,10 @@ export default function SubjectWorkbench() { ...@@ -144,7 +141,10 @@ export default function SubjectWorkbench() {
view: activeView, view: activeView,
page, page,
pageSize, pageSize,
keyword: debouncedKeyword, filters: {
...filters,
__fieldDefs: Object.fromEntries(columns.map((column) => [column.field, column.fieldDef])),
},
pagination: paginationEnabled, pagination: paginationEnabled,
}) })
.then((result) => { .then((result) => {
...@@ -162,7 +162,7 @@ export default function SubjectWorkbench() { ...@@ -162,7 +162,7 @@ export default function SubjectWorkbench() {
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [subjectName, activeView, page, pageSize, debouncedKeyword, paginationEnabled, refreshKey]); }, [subjectName, activeView, page, pageSize, filters, columns, paginationEnabled, refreshKey]);
const runAction = useCallback( const runAction = useCallback(
async (actionName, context, assignment = null) => { async (actionName, context, assignment = null) => {
...@@ -283,21 +283,14 @@ export default function SubjectWorkbench() { ...@@ -283,21 +283,14 @@ export default function SubjectWorkbench() {
setSelectedIds(EMPTY_ARRAY); setSelectedIds(EMPTY_ARRAY);
setPage(0); setPage(0);
}} }}
filters={{ keyword }} filters={filters}
filterSchema={[ filterSchema={filterSchema}
{ onFiltersChange={(key, value) => {
key: 'keyword', setFilters((current) => ({ ...current, [key]: value }));
label: '查找记录',
type: 'text',
placeholder: '搜索标题',
},
]}
onFiltersChange={(_, value) => {
setKeyword(value);
setPage(0); setPage(0);
}} }}
onClearFilters={() => { onClearFilters={() => {
setKeyword(''); setFilters({});
setPage(0); setPage(0);
}} }}
title={subjectConfig.title || subject?.title || subjectName} title={subjectConfig.title || subject?.title || subjectName}
......
...@@ -23,11 +23,11 @@ ...@@ -23,11 +23,11 @@
1. 进入页面后并行调用 `GET /subject/:name` 和 `GET /subject/:name/view`;打开详情后再按 View 名调用 `GET /subject/:subjectName/view/:viewName` 获取完整 Detail 配置。 1. 进入页面后并行调用 `GET /subject/:name` 和 `GET /subject/:name/view`;打开详情后再按 View 名调用 `GET /subject/:subjectName/view/:viewName` 获取完整 Detail 配置。
2. 只展示当前用户有权访问的 `grid` 视图;授权只读取 `user.applications[]` 中 `name = integrity_practice_management` 的项目。系统管理员、系统账号和该应用“管理”角色可旁路,但不能旁路 `disableColumns`。配置包含 `filterView` 且非空时,只展示指定视图。 2. 只展示当前用户有权访问的 `grid` 视图;授权只读取 `user.applications[]` 中 `name = integrity_practice_management` 的项目。系统管理员、系统账号和该应用“管理”角色可旁路,但不能旁路 `disableColumns`。配置包含 `filterView` 且非空时,只展示指定视图。
3. 切换视图后,根据视图的 `settings.restrictions`、`settings.filters.conditions`、列排序配置调用 `POST /record/query`。 3. 切换视图后,根据视图的 `settings.restrictions`、`settings.filters.conditions`、列排序配置调用 `POST /record/query`;筛选控件由当前 View 的可见字段自动生成,文本字段使用远程模糊查询,Select/日期字段使用字段配置值。
4. `gridRecordConfig.pagination = false` 时单次最多读取 1000 条;其余页面使用服务端分页。 4. `gridRecordConfig.pagination = false` 时单次最多读取 1000 条;其余页面使用服务端分页。
5. 关键词查找通过 `/record/query` 的 `keyword` 查询标题;清空筛选后回到第 1 页。 5. 列表不再提供独立关键词搜索;用户通过当前视图字段筛选,筛选条件直接写入 `/record/query` 的 `filter`,清空筛选后回到第 1 页。
6. 列定义、列顺序、初始排序、固定列和字段显示格式由 Subject View 与 Subject 字段定义共同决定;HeroUI Pro DataGrid 仅在真实横向溢出时固定首列和操作列,并允许当前页原生列排序和列宽调整,不改变服务端分页口径。 6. 列定义、列顺序、初始排序、固定列和字段显示格式由 Subject View 与 Subject 字段定义共同决定;HeroUI Pro DataGrid 仅在真实横向溢出时固定首列和操作列,并允许当前页原生列排序和列宽调整,不改变服务端分页口径。
`settings.shortTitle === true` 时只隐藏 title 展示列,标题仍参与查询、记录身份和保存;false 或未配置时正常展示。 `settings.shortTitle === true` 时只隐藏 title 展示列,标题仍作为记录身份和保存字段;false 或未配置时正常展示。若 View 配置包含标题列,标题也会作为可筛选字段显示。
7. DataGrid 使用原生 Checkbox 选择;选中后显示 HeroUI Pro ActionBar。行操作 1-3 项直接展示,4 项以上展示前两项与“更多”菜单,删除等危险动作始终排在最后。 7. DataGrid 使用原生 Checkbox 选择;选中后显示 HeroUI Pro ActionBar。行操作 1-3 项直接展示,4 项以上展示前两项与“更多”菜单,删除等危险动作始终排在最后。
8. 点击列表操作列的预览图标打开 HeroUI Modal 并进入只读查看态;首个业务列只展示记录文本,不再承担预览交互。点击弹窗统一的“编辑”按钮或列表操作列铅笔图标进入整体编辑态,保存后返回查看态。字段权限、锁定规则和 Subject 字段类型保持不变。 8. 点击列表操作列的预览图标打开 HeroUI Modal 并进入只读查看态;首个业务列只展示记录文本,不再承担预览交互。点击弹窗统一的“编辑”按钮或列表操作列铅笔图标进入整体编辑态,保存后返回查看态。字段权限、锁定规则和 Subject 字段类型保持不变。
9. 当前 Detail 有有效 `settings.auth` 或 `settings.disableColumns` 时,字段权限整套使用 Detail;两项都未配置或没有实际 Detail 时,整体回退打开详情的当前 Grid。两套权限不合并、不逐字段回退;`disableColumns` 对管理员也始终只读,普通用户在选定权限来源中缺少字段 `auth` 时只读。切换 Detail 后立即重算字段状态和编辑入口,编辑入口只检查当前 Detail 展示字段。Grid `viewAuth` 只控制 Tab 可见性,不隐藏详情字段。 9. 当前 Detail 有有效 `settings.auth` 或 `settings.disableColumns` 时,字段权限整套使用 Detail;两项都未配置或没有实际 Detail 时,整体回退打开详情的当前 Grid。两套权限不合并、不逐字段回退;`disableColumns` 对管理员也始终只读,普通用户在选定权限来源中缺少字段 `auth` 时只读。切换 Detail 后立即重算字段状态和编辑入口,编辑入口只检查当前 Detail 展示字段。Grid `viewAuth` 只控制 Tab 可见性,不隐藏详情字段。
......
...@@ -17,6 +17,10 @@ import { ...@@ -17,6 +17,10 @@ import {
import DocumentService from '@/services/DocumentService'; import DocumentService from '@/services/DocumentService';
import { import {
buildViewColumns, buildViewColumns,
getSelectOptions,
isDateFieldType,
isReferenceFieldType,
getReferenceSubjectName,
checkViewCondition, checkViewCondition,
isViewAccessible, isViewAccessible,
mergeRestrictionsIntoFilter, mergeRestrictionsIntoFilter,
...@@ -31,7 +35,7 @@ import * as XLSX from 'xlsx'; ...@@ -31,7 +35,7 @@ import * as XLSX from 'xlsx';
export const DEFAULT_PAGE_SIZE = 10; export const DEFAULT_PAGE_SIZE = 10;
function buildViewFilter(view) { function buildViewFilter(view, filters = {}) {
const restrictions = view?.settings?.restrictions || []; const restrictions = view?.settings?.restrictions || [];
const filter = mergeRestrictionsIntoFilter({}, restrictions); const filter = mergeRestrictionsIntoFilter({}, restrictions);
const conditions = view?.settings?.filters?.conditions || []; const conditions = view?.settings?.filters?.conditions || [];
...@@ -46,6 +50,14 @@ function buildViewFilter(view) { ...@@ -46,6 +50,14 @@ function buildViewFilter(view) {
filter[condition.field] = comparator === '$eq' ? value : { [comparator]: value }; filter[condition.field] = comparator === '$eq' ? value : { [comparator]: value };
}); });
Object.entries(filters).forEach(([field, value]) => {
if (!field || field === '__fieldDefs' || value === undefined || value === null || String(value).trim() === '') return;
const type = String(filters.__fieldDefs?.[field]?.type || '').toLowerCase();
filter[field] = ['text', 'long_text', 'rich_text'].includes(type)
? { $regex: String(value).trim(), $options: 'i' }
: Array.isArray(value) ? { $in: value } : value;
});
return filter; return filter;
} }
...@@ -90,7 +102,7 @@ export async function loadSubjectRecords({ ...@@ -90,7 +102,7 @@ export async function loadSubjectRecords({
view, view,
page = 0, page = 0,
pageSize = DEFAULT_PAGE_SIZE, pageSize = DEFAULT_PAGE_SIZE,
keyword = '', filters = {},
pagination = true, pagination = true,
}) { }) {
const requestPageSize = pagination ? pageSize : 1000; const requestPageSize = pagination ? pageSize : 1000;
...@@ -98,9 +110,8 @@ export async function loadSubjectRecords({ ...@@ -98,9 +110,8 @@ export async function loadSubjectRecords({
subject: subjectName, subject: subjectName,
page: pagination ? page : 0, page: pagination ? page : 0,
page_size: requestPageSize, page_size: requestPageSize,
filter: buildViewFilter(view), filter: buildViewFilter(view, filters),
sort: buildViewSort(view), sort: buildViewSort(view),
keyword: normalizeText(keyword) || undefined,
mode: 'full', mode: 'full',
}); });
const records = sortRecordsByView(toArray(result?.records), view); const records = sortRecordsByView(toArray(result?.records), view);
...@@ -123,6 +134,15 @@ export function buildSubjectColumns(view, subject) { ...@@ -123,6 +134,15 @@ export function buildSubjectColumns(view, subject) {
return buildViewColumns(view, subject); return buildViewColumns(view, subject);
} }
export function buildSubjectFilterSchema(view, subject) {
return buildViewColumns(view, subject).filter((column) => column?.field).map((column) => {
const fieldDef = column.fieldDef || {};
const type = String(fieldDef.type || 'text').toLowerCase();
const options = getSelectOptions(fieldDef);
return { key: column.field, field: column.field, label: column.label || fieldDef.label || column.field, type: type === 'select' && options.length ? 'select' : isDateFieldType(type) ? 'date' : isReferenceFieldType(type) && getReferenceSubjectName(fieldDef) ? 'reference' : 'text', options, fieldDef, placeholder: `筛选${column.label || fieldDef.label || column.field}` };
});
}
export function isConfiguredActionVisible(action, record) { export function isConfiguredActionVisible(action, record) {
return checkViewCondition(action?.visabled, record?.raw || record); return checkViewCondition(action?.visabled, record?.raw || record);
} }
......
...@@ -252,14 +252,12 @@ export function isDateFieldType(fieldType) { ...@@ -252,14 +252,12 @@ export function isDateFieldType(fieldType) {
} }
export function normalizeRecordId(value) { export function normalizeRecordId(value) {
if (!value) { if (value === null || value === undefined || value === '') return '';
return '';
}
if (typeof value === 'object') { if (typeof value === 'object') {
return value._id || value.id || value.name || ''; const candidate = value._id ?? value.id ?? value.$oid ?? value.name;
if (candidate === value) return '';
return normalizeRecordId(candidate);
} }
return normalizeText(value); return normalizeText(value);
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment