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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { StreamingTimelineComponent } from '../../../../shared/streaming-timelin
import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts';
import { COMPUTED_FUNCTIONS_SPECS } from './specs';
import { highlightJson } from '../../../../shared/json-highlight';
import { toDisplayText } from '../../../../shared/to-display-text';

// --- Inline view components registered in the demo registry ---

Expand Down Expand Up @@ -73,10 +74,7 @@ class DemoValueComponent {
})
class DemoHeadingComponent {
readonly content = input<unknown>('');
readonly displayContent = computed(() => {
const c = this.content();
return typeof c === 'string' ? c : '';
});
readonly displayContent = computed(() => toDisplayText(this.content()));
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | null>(null);
readonly bindings = input<Record<string, string>>({});
Expand Down
14 changes: 7 additions & 7 deletions cockpit/render/computed-functions/angular/src/app/specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ export const COMPUTED_FUNCTIONS_SPECS: DemoSpec[] = [
type: 'Value',
props: {
label: 'Uppercase',
value: { $fn: 'uppercase', args: { value: 'hello world' } },
value: { $computed: 'uppercase', args: { value: 'hello world' } },
},
},
reversed: {
type: 'Value',
props: {
label: 'Reversed',
value: { $fn: 'reverse', args: { value: 'streaming' } },
value: { $computed: 'reverse', args: { value: 'streaming' } },
},
},
},
Expand All @@ -43,14 +43,14 @@ export const COMPUTED_FUNCTIONS_SPECS: DemoSpec[] = [
type: 'Value',
props: {
label: 'Formatted Date',
value: { $fn: 'formatDate', args: { value: '2024-06-15T12:00:00Z' } },
value: { $computed: 'formatDate', args: { value: '2024-06-15T12:00:00Z' } },
},
},
product: {
type: 'Value',
props: {
label: 'Multiply 7 x 6',
value: { $fn: 'multiply', args: { a: 7, b: 6 } },
value: { $computed: 'multiply', args: { a: 7, b: 6 } },
},
},
},
Expand All @@ -70,21 +70,21 @@ export const COMPUTED_FUNCTIONS_SPECS: DemoSpec[] = [
type: 'Value',
props: {
label: 'Multiply 12 x 5',
value: { $fn: 'multiply', args: { a: 12, b: 5 } },
value: { $computed: 'multiply', args: { a: 12, b: 5 } },
},
},
transform: {
type: 'Value',
props: {
label: 'Uppercase',
value: { $fn: 'uppercase', args: { value: 'computed functions' } },
value: { $computed: 'uppercase', args: { value: 'computed functions' } },
},
},
format: {
type: 'Value',
props: {
label: 'Date',
value: { $fn: 'formatDate', args: { value: '2025-01-01T00:00:00Z' } },
value: { $computed: 'formatDate', args: { value: '2025-01-01T00:00:00Z' } },
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { StreamingTimelineComponent } from '../../../../shared/streaming-timelin
import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts';
import { ELEMENT_RENDERING_SPECS } from './specs';
import { highlightJson } from '../../../../shared/json-highlight';
import { toDisplayText } from '../../../../shared/to-display-text';

// --- Inline view components registered in the demo registry ---

Expand All @@ -30,10 +31,7 @@ import { highlightJson } from '../../../../shared/json-highlight';
})
class DemoTextComponent {
readonly content = input<unknown>('');
readonly displayContent = computed(() => {
const c = this.content();
return typeof c === 'string' ? c : '';
});
readonly displayContent = computed(() => toDisplayText(this.content()));
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | null>(null);
readonly bindings = input<Record<string, string>>({});
Expand All @@ -59,10 +57,7 @@ class DemoTextComponent {
})
class DemoHeadingComponent {
readonly content = input<unknown>('');
readonly displayContent = computed(() => {
const c = this.content();
return typeof c === 'string' ? c : '';
});
readonly displayContent = computed(() => toDisplayText(this.content()));
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | null>(null);
readonly bindings = input<Record<string, string>>({});
Expand Down
11 changes: 3 additions & 8 deletions cockpit/render/registry/angular/src/app/registry.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { StreamingTimelineComponent } from '../../../../shared/streaming-timelin
import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts';
import { REGISTRY_SPECS } from './specs';
import { highlightJson } from '../../../../shared/json-highlight';
import { toDisplayText } from '../../../../shared/to-display-text';

// --- Inline view components registered in the demo registry ---

Expand All @@ -30,10 +31,7 @@ import { highlightJson } from '../../../../shared/json-highlight';
})
class DemoTextComponent {
readonly content = input<unknown>('');
readonly displayContent = computed(() => {
const c = this.content();
return typeof c === 'string' ? c : '';
});
readonly displayContent = computed(() => toDisplayText(this.content()));
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | null>(null);
readonly bindings = input<Record<string, string>>({});
Expand All @@ -59,10 +57,7 @@ class DemoTextComponent {
})
class DemoHeadingComponent {
readonly content = input<unknown>('');
readonly displayContent = computed(() => {
const c = this.content();
return typeof c === 'string' ? c : '';
});
readonly displayContent = computed(() => toDisplayText(this.content()));
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | null>(null);
readonly bindings = input<Record<string, string>>({});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { StreamingTimelineComponent } from '../../../../shared/streaming-timelin
import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts';
import { REPEAT_LOOPS_SPECS } from './specs';
import { highlightJson } from '../../../../shared/json-highlight';
import { toDisplayText } from '../../../../shared/to-display-text';

// --- Inline view components registered in the demo registry ---

Expand All @@ -30,10 +31,7 @@ import { highlightJson } from '../../../../shared/json-highlight';
})
class DemoTextComponent {
readonly content = input<unknown>('');
readonly displayContent = computed(() => {
const c = this.content();
return typeof c === 'string' ? c : '';
});
readonly displayContent = computed(() => toDisplayText(this.content()));
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | null>(null);
readonly bindings = input<Record<string, string>>({});
Expand All @@ -59,10 +57,7 @@ class DemoTextComponent {
})
class DemoHeadingComponent {
readonly content = input<unknown>('');
readonly displayContent = computed(() => {
const c = this.content();
return typeof c === 'string' ? c : '';
});
readonly displayContent = computed(() => toDisplayText(this.content()));
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | null>(null);
readonly bindings = input<Record<string, string>>({});
Expand Down
1 change: 0 additions & 1 deletion cockpit/render/shared/streaming-timeline.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ import { StreamingSimulator } from './streaming-simulator';
border: 2px solid var(--tl-green-bright);
transform: translate(-50%, -50%);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
transition: left 0.075s linear;
}
.tl__count {
flex-shrink: 0;
Expand Down
33 changes: 33 additions & 0 deletions cockpit/render/shared/to-display-text.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
import { toDisplayText } from './to-display-text';

describe('toDisplayText', () => {
it('returns strings unchanged', () => {
expect(toDisplayText('Alice')).toBe('Alice');
expect(toDisplayText('')).toBe('');
});

it('stringifies numbers (the bug: numeric $state bindings were dropped)', () => {
expect(toDisplayText(30)).toBe('30');
expect(toDisplayText(42.5)).toBe('42.5');
});

it('preserves zero (falsy but must display)', () => {
expect(toDisplayText(0)).toBe('0');
});

it('stringifies booleans', () => {
expect(toDisplayText(true)).toBe('true');
expect(toDisplayText(false)).toBe('false');
});

it('returns empty string for null / undefined', () => {
expect(toDisplayText(null)).toBe('');
expect(toDisplayText(undefined)).toBe('');
});

it('returns empty string for objects and arrays (not display text)', () => {
expect(toDisplayText({ a: 1 })).toBe('');
expect(toDisplayText([1, 2])).toBe('');
});
});
19 changes: 19 additions & 0 deletions cockpit/render/shared/to-display-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT

/**
* Coerce a resolved element prop (which may be a string, number, boolean, or —
* for an unresolved/object binding — something else) into display text.
*
* Demo view components bind props like `content`/`value` that can resolve to
* non-string primitives via `$state`/`$fn` bindings (e.g. a numeric
* `/user/age`). Returning `''` only for strings silently dropped those values;
* this renders any primitive and treats objects/null as "no text".
*/
export function toDisplayText(value: unknown): string {
if (value == null) return '';
const t = typeof value;
if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint') {
return String(value);
}
return '';
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { StreamingTimelineComponent } from '../../../../shared/streaming-timelin
import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts';
import { SPEC_RENDERING_SPECS } from './specs';
import { highlightJson } from '../../../../shared/json-highlight';
import { toDisplayText } from '../../../../shared/to-display-text';

// --- Inline view components registered in the demo registry ---

Expand All @@ -30,10 +31,7 @@ import { highlightJson } from '../../../../shared/json-highlight';
})
class DemoTextComponent {
readonly content = input<unknown>('');
readonly displayContent = computed(() => {
const c = this.content();
return typeof c === 'string' ? c : '';
});
readonly displayContent = computed(() => toDisplayText(this.content()));
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | null>(null);
readonly bindings = input<Record<string, string>>({});
Expand All @@ -59,10 +57,7 @@ class DemoTextComponent {
})
class DemoHeadingComponent {
readonly content = input<unknown>('');
readonly displayContent = computed(() => {
const c = this.content();
return typeof c === 'string' ? c : '';
});
readonly displayContent = computed(() => toDisplayText(this.content()));
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | null>(null);
readonly bindings = input<Record<string, string>>({});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { StreamingTimelineComponent } from '../../../../shared/streaming-timelin
import { ExampleSplitLayoutComponent } from '@threadplane/example-layouts';
import { STATE_MANAGEMENT_SPECS } from './specs';
import { highlightJson } from '../../../../shared/json-highlight';
import { toDisplayText } from '../../../../shared/to-display-text';

// --- Inline view components registered in the demo registry ---

Expand All @@ -30,10 +31,7 @@ import { highlightJson } from '../../../../shared/json-highlight';
})
class DemoTextComponent {
readonly content = input<unknown>('');
readonly displayContent = computed(() => {
const c = this.content();
return typeof c === 'string' ? c : '';
});
readonly displayContent = computed(() => toDisplayText(this.content()));
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | null>(null);
readonly bindings = input<Record<string, string>>({});
Expand All @@ -59,10 +57,7 @@ class DemoTextComponent {
})
class DemoHeadingComponent {
readonly content = input<unknown>('');
readonly displayContent = computed(() => {
const c = this.content();
return typeof c === 'string' ? c : '';
});
readonly displayContent = computed(() => toDisplayText(this.content()));
readonly childKeys = input<string[]>([]);
readonly spec = input<Spec | null>(null);
readonly bindings = input<Record<string, string>>({});
Expand Down
Loading