Fix array label properties display

This commit is contained in:
Maurus Decimus
2026-04-21 15:44:00 +02:00
parent cfb3a308cd
commit 35e0ef74b9
6 changed files with 43 additions and 10 deletions
+9
View File
@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
## [0.1.1] - 2026-04-XX
### Added
### Changed
### Fixed
- Fix array label properties display.
## [0.1.0] - 2026-04-20 ## [0.1.0] - 2026-04-20
### Added ### Added
+11 -3
View File
@@ -48,8 +48,14 @@ export function ObjectPicker({ schema, objectName, value, onChange, onClear, pla
return ( return (
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
{value && ( {value && (
<Badge variant="secondary" className="gap-1 pr-1.5 text-sm"> <Badge variant="secondary" className="gap-1 pr-1.5 text-sm max-w-xs">
{labelLoading ? <Loader2 className="h-3 w-3 animate-spin" /> : (display ?? value)} {labelLoading ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<span className="truncate" title={display ?? value}>
{display ?? value}
</span>
)}
{onClear && ( {onClear && (
<button <button
type="button" type="button"
@@ -121,7 +127,9 @@ export function ObjectPicker({ schema, objectName, value, onChange, onClear, pla
setOpen(false); setOpen(false);
}} }}
> >
{opt.label} <span className="truncate" title={opt.label}>
{opt.label}
</span>
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
+4 -3
View File
@@ -47,6 +47,7 @@ import {
import { jmapGet, jmapSet, jmapRequest, getAccountId } from '@/services/jmap/client'; import { jmapGet, jmapSet, jmapRequest, getAccountId } from '@/services/jmap/client';
import { calculateJmapPatch } from '@/lib/jmapPatch'; import { calculateJmapPatch } from '@/lib/jmapPatch';
import { friendlySetError } from '@/lib/jmapErrors'; import { friendlySetError } from '@/lib/jmapErrors';
import { coerceLabel } from '@/lib/objectOptions';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { logFormChange } from '@/lib/debug'; import { logFormChange } from '@/lib/debug';
import { FieldWidget } from '@/components/forms/FieldWidget'; import { FieldWidget } from '@/components/forms/FieldWidget';
@@ -695,9 +696,9 @@ export function DynamicForm({ viewName, objectId }: DynamicFormProps) {
const labelProp = list?.labelProperty; const labelProp = list?.labelProperty;
if (labelProp) { if (labelProp) {
const raw = formData[labelProp]; const value = coerceLabel(formData[labelProp], '');
if (typeof raw === 'string' && raw.length > 0) { if (value.length > 0) {
return t('form.editTitleWithValue', 'Edit {{name}}: {{value}}', { name, value: raw }); return t('form.editTitleWithValue', 'Edit {{name}}: {{value}}', { name, value });
} }
} }
return t('form.editTitle', 'Edit {{name}}', { name }); return t('form.editTitle', 'Edit {{name}}', { name });
+2 -1
View File
@@ -48,6 +48,7 @@ import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/component
import { ObjectPicker } from '@/components/common/ObjectPicker'; import { ObjectPicker } from '@/components/common/ObjectPicker';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { friendlySetError } from '@/lib/jmapErrors'; import { friendlySetError } from '@/lib/jmapErrors';
import { coerceLabel } from '@/lib/objectOptions';
import { useSchemaStore } from '@/stores/schemaStore'; import { useSchemaStore } from '@/stores/schemaStore';
import { useAuthStore } from '@/stores/authStore'; import { useAuthStore } from '@/stores/authStore';
@@ -515,7 +516,7 @@ export function DynamicList({ viewName }: DynamicListProps) {
for (const obj of list) { for (const obj of list) {
const id = obj.id as string; const id = obj.id as string;
if (!id) continue; if (!id) continue;
entries[id] = (obj[displayProp] as string) ?? id; entries[id] = coerceLabel(obj[displayProp], id);
} }
if (Object.keys(entries).length > 0) { if (Object.keys(entries).length > 0) {
setDisplayNames(refType, entries); setDisplayNames(refType, entries);
+3 -1
View File
@@ -11,6 +11,7 @@ import { ArrowLeft, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useSchemaStore } from '@/stores/schemaStore'; import { useSchemaStore } from '@/stores/schemaStore';
import { resolveObject, resolveList } from '@/lib/schemaResolver'; import { resolveObject, resolveList } from '@/lib/schemaResolver';
import { coerceLabel } from '@/lib/objectOptions';
import { jmapGet, getAccountId } from '@/services/jmap/client'; import { jmapGet, getAccountId } from '@/services/jmap/client';
import { DynamicView } from './DynamicView'; import { DynamicView } from './DynamicView';
@@ -89,7 +90,8 @@ export function DynamicViewPage({ viewName, objectId }: DynamicViewPageProps) {
const list = resolveList(schema, viewName, resolved.objectName); const list = resolveList(schema, viewName, resolved.objectName);
const labelProp = list?.labelProperty ?? list?.columns?.[0]?.name; const labelProp = list?.labelProperty ?? list?.columns?.[0]?.name;
const displayName = labelProp && typeof data[labelProp] === 'string' ? (data[labelProp] as string) : undefined; const rawLabel = labelProp ? coerceLabel(data[labelProp], '') : '';
const displayName = rawLabel.length > 0 ? rawLabel : undefined;
const singularName = list?.singularName ?? resolved.objectName.replace(/^x:/, ''); const singularName = list?.singularName ?? resolved.objectName.replace(/^x:/, '');
const title = displayName const title = displayName
? `${singularName.charAt(0).toUpperCase() + singularName.slice(1)}: ${displayName}` ? `${singularName.charAt(0).toUpperCase() + singularName.slice(1)}: ${displayName}`
+14 -2
View File
@@ -21,6 +21,18 @@ interface PendingBatch {
const pendingByType = new Map<string, PendingBatch>(); const pendingByType = new Map<string, PendingBatch>();
export function coerceLabel(value: unknown, fallback: string): string {
if (value == null) return fallback;
if (typeof value === 'string') return value;
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
if (Array.isArray(value)) return value.length > 0 ? value.map((v) => coerceLabel(v, '')).filter(Boolean).join(', ') : fallback;
if (typeof value === 'object') {
const keys = Object.keys(value as Record<string, unknown>);
return keys.length > 0 ? keys.join(', ') : fallback;
}
return fallback;
}
function requestObjectLabel(parentObjectName: string, viewOrObjectName: string, id: string, schema: Schema): void { function requestObjectLabel(parentObjectName: string, viewOrObjectName: string, id: string, schema: Schema): void {
const existing = pendingByType.get(parentObjectName); const existing = pendingByType.get(parentObjectName);
if (existing) { if (existing) {
@@ -54,7 +66,7 @@ async function executeBatch(parentObjectName: string, batch: PendingBatch): Prom
for (const item of list) { for (const item of list) {
const itemId = item.id as string; const itemId = item.id as string;
if (!itemId) continue; if (!itemId) continue;
entries[itemId] = (item[batch.displayProp] as string) ?? itemId; entries[itemId] = coerceLabel(item[batch.displayProp], itemId);
} }
for (const id of ids) { for (const id of ids) {
if (!(id in entries)) entries[id] = id; if (!(id in entries)) entries[id] = id;
@@ -95,7 +107,7 @@ async function fetchObjectList(viewOrObjectName: string, schema: Schema): Promis
return items.map((item) => ({ return items.map((item) => ({
id: item.id as string, id: item.id as string,
label: (item[displayProp] as string) ?? (item.id as string), label: coerceLabel(item[displayProp], item.id as string),
})); }));
} }