Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/app-shell/src/views/CreateViewDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
Clock,
Map as MapIcon,
BarChart3,
ListTree,
AlertCircle,
} from 'lucide-react';

Expand Down Expand Up @@ -87,6 +88,7 @@ function buildViewTypeMeta(t: (k: string) => string): ViewTypeMeta[] {
{ type: 'gantt', icon: GanttChartSquare, label: t('console.objectView.viewTypeGantt'), description: t('console.objectView.viewTypeGanttDesc') },
{ type: 'map', icon: MapIcon, label: t('console.objectView.viewTypeMap'), description: t('console.objectView.viewTypeMapDesc') },
{ type: 'chart', icon: BarChart3, label: t('console.objectView.viewTypeChart'), description: t('console.objectView.viewTypeChartDesc') },
{ type: 'tree', icon: ListTree, label: t('console.objectView.viewTypeTree'), description: t('console.objectView.viewTypeTreeDesc') },
];
}

Expand Down Expand Up @@ -254,6 +256,19 @@ const REQUIRED_FIELDS_BY_TYPE: Record<string, RequiredFieldDef[]> = {
filter: (f) => f.type === 'number',
},
],
tree: [
{
key: 'parentField',
i18nKey: 'console.objectView.parentField',
helpI18nKey: 'console.objectView.parentFieldHelp',
// Self-referencing pointer: a `tree` field, or a lookup/master_detail
// back to the same object. `rawType` carries the unnormalized field type.
filter: (f) =>
f.rawType === 'tree' ||
f.rawType === 'lookup' ||
f.rawType === 'master_detail',
},
],
// grid has no strictly required fields at create time
};

Expand Down
4 changes: 4 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,10 @@ const en = {
viewTypeMapDesc: 'Geographic markers from latitude / longitude fields.',
viewTypeChart: 'Chart',
viewTypeChartDesc: 'Aggregated bar / line / pie visualisations.',
viewTypeTree: 'Tree',
viewTypeTreeDesc: 'Nest self-referencing records into a hierarchy by a parent field.',
parentField: 'Parent field',
parentFieldHelp: 'The field pointing to the parent record (same object) that defines the hierarchy. Only self-referencing fields qualify.',
newView: 'New View',
typeOptions: 'Type Options',
groupByField: 'Group by field',
Expand Down
4 changes: 4 additions & 0 deletions packages/i18n/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,10 @@ const zh = {
viewTypeMapDesc: '根据经纬度字段显示地理标记。',
viewTypeChart: '图表',
viewTypeChartDesc: '聚合的柱状/折线/饼状图。',
viewTypeTree: '树形',
viewTypeTreeDesc: '按父级字段把自引用记录嵌套成层级树。',
parentField: '父级字段',
parentFieldHelp: '指向同一对象的父级字段,决定树的层级(仅自引用字段可选)。',
newView: '新视图',
typeOptions: '类型选项',
groupByField: '分组字段',
Expand Down
26 changes: 26 additions & 0 deletions packages/plugin-tree/src/ObjectTree.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,32 @@ describe('ObjectTree', () => {
expect(screen.queryByText('Engineering')).toBeNull();
});

it('accepts field entries as objects (host columns), not just strings', async () => {
// ListView passes columns as field *objects*; feeding those straight into
// `.replace()` threw "e.replace is not a function" and failed to render.
render(
<ObjectTree
schema={{
type: 'object-tree',
objectName: 'business_unit',
parentField: 'parent_id',
labelField: { name: 'name', label: 'Name' },
fields: [
{ name: 'name', label: 'Name' },
{ fieldName: 'head', label: 'Head' },
],
data: orgUnits,
}}
data={orgUnits}
/>,
);
await waitFor(() => expect(screen.getByTestId('object-tree')).toBeTruthy());
expect(screen.getByText('Acme')).toBeTruthy();
// The object-shaped `head` column still renders its values.
expect(screen.getByText('VP Eng')).toBeTruthy();
expect(screen.getByText('Head')).toBeTruthy();
});

it('keeps orphan records (parent outside the result set) as roots', async () => {
const orphans = [
{ id: '10', name: 'Floating', parent_id: '999' },
Expand Down
29 changes: 22 additions & 7 deletions packages/plugin-tree/src/ObjectTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,31 @@ function getDataConfig(schema: any): ViewData | null {
return null;
}

/**
* Normalize a field entry to its string key. Hosts like ListView pass columns
* as field *objects* (`{ name | fieldName | field, label, … }`), not bare
* strings — feeding those straight into `.replace()`/record indexing throws
* ("e.replace is not a function"). Accept both shapes here so the tree is
* resilient regardless of caller.
*/
function fieldKey(f: any): string | undefined {
if (typeof f === 'string') return f;
if (f && typeof f === 'object') return f.name || f.fieldName || f.field || f.key;
return undefined;
}

function getTreeConfig(schema: any): TreeConfig {
const nested = (schema.tree || schema.filter?.tree || {}) as Partial<TreeConfig>;
const rawFields = Array.isArray(schema.fields)
? schema.fields
: Array.isArray(nested.fields)
? nested.fields
: [];
return {
parentField: schema.parentField ?? nested.parentField,
labelField: schema.labelField ?? nested.labelField ?? schema.titleField ?? 'name',
fields: Array.isArray(schema.fields)
? schema.fields
: Array.isArray(nested.fields)
? nested.fields
: [],
parentField: fieldKey(schema.parentField ?? nested.parentField),
labelField:
fieldKey(schema.labelField ?? nested.labelField ?? schema.titleField) ?? 'name',
fields: rawFields.map(fieldKey).filter((f): f is string => !!f),
defaultExpandedDepth: schema.defaultExpandedDepth ?? nested.defaultExpandedDepth,
};
}
Expand Down
Loading