fix(editor): render container subflows and long port labels
This commit is contained in:
parent
15b411c992
commit
14f78b79ce
|
|
@ -0,0 +1,89 @@
|
|||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { environment } from '@environment';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { AssistantCallService } from './assistant-call';
|
||||
|
||||
describe('AssistantCallService', () => {
|
||||
let service: AssistantCallService;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
AssistantCallService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting()
|
||||
]
|
||||
});
|
||||
|
||||
service = TestBed.inject(AssistantCallService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('normalizes node families in assistant drafts and nested container subflows', async () => {
|
||||
const request = firstValueFrom(service.getSession('session-1'));
|
||||
|
||||
httpMock.expectOne(`${environment.apiUrl}/assistant/sessions/session-1`).flush({
|
||||
id: 'session-1',
|
||||
selectedModel: 'model-1',
|
||||
messages: [],
|
||||
currentDraftFlow: {
|
||||
name: 'Loop draft',
|
||||
flow: {
|
||||
blocks: [{ id: 'root-block', typeName: 'LLMBlock', specificConfiguration: {} }],
|
||||
containers: [{
|
||||
id: 'loop-1',
|
||||
typeName: 'LoopContainer',
|
||||
specificConfiguration: {
|
||||
subFlow: {
|
||||
blocks: [{ id: 'body-block', typeName: 'LLMBlock', specificConfiguration: {} }],
|
||||
containers: [{
|
||||
id: 'nested-container',
|
||||
typeName: 'GenericContainer',
|
||||
specificConfiguration: {
|
||||
subFlow: { blocks: [], containers: [], connections: [], dependencies: [] }
|
||||
}
|
||||
}],
|
||||
connections: [],
|
||||
dependencies: []
|
||||
},
|
||||
guardSubFlow: {
|
||||
blocks: [{ id: 'guard-block', typeName: 'SwitchBlock', specificConfiguration: {} }],
|
||||
containers: [],
|
||||
connections: [],
|
||||
dependencies: []
|
||||
}
|
||||
}
|
||||
}],
|
||||
connections: [],
|
||||
dependencies: []
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const session = await request;
|
||||
const flow = session.currentDraftFlow!.flow;
|
||||
const loopConfiguration = flow.containers[0].specificConfiguration as Record<string, any>;
|
||||
|
||||
expect(flow.blocks[0].nodeFamily).toBe('block');
|
||||
expect(flow.containers[0].nodeFamily).toBe('container');
|
||||
expect(loopConfiguration['subFlow'].blocks[0].nodeFamily).toBe('block');
|
||||
expect(loopConfiguration['subFlow'].containers[0].nodeFamily).toBe('container');
|
||||
expect(loopConfiguration['subFlow'].containers[0].specificConfiguration.subFlow).toEqual({
|
||||
blocks: [],
|
||||
containers: [],
|
||||
connections: [],
|
||||
dependencies: [],
|
||||
globalInputs: [],
|
||||
lanes: []
|
||||
});
|
||||
expect(loopConfiguration['guardSubFlow'].blocks[0].nodeFamily).toBe('block');
|
||||
});
|
||||
});
|
||||
|
|
@ -203,14 +203,63 @@ function hasAssistantFlowData(flow: AssistantDraftPayload['flow']): boolean {
|
|||
function mapAssistantFlowData(raw: unknown): AssistantDraftPayload['flow'] {
|
||||
const value = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
|
||||
return {
|
||||
blocks: Array.isArray(value['blocks']) ? (value['blocks'] as any[]) : [],
|
||||
containers: Array.isArray(value['containers']) ? (value['containers'] as any[]) : [],
|
||||
blocks: normalizeAssistantFlowNodes(value['blocks'], 'block'),
|
||||
containers: normalizeAssistantFlowNodes(value['containers'], 'container'),
|
||||
connections: Array.isArray(value['connections']) ? (value['connections'] as any[]) : [],
|
||||
dependencies: Array.isArray(value['dependencies']) ? (value['dependencies'] as any[]) : [],
|
||||
globalInputs: Array.isArray(value['globalInputs']) ? (value['globalInputs'] as any[]) : []
|
||||
globalInputs: Array.isArray(value['globalInputs']) ? (value['globalInputs'] as any[]) : [],
|
||||
lanes: Array.isArray(value['lanes']) ? (value['lanes'] as any[]) : []
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAssistantFlowNodes(
|
||||
raw: unknown,
|
||||
nodeFamily: 'block'
|
||||
): AssistantDraftPayload['flow']['blocks'];
|
||||
function normalizeAssistantFlowNodes(
|
||||
raw: unknown,
|
||||
nodeFamily: 'container'
|
||||
): AssistantDraftPayload['flow']['containers'];
|
||||
function normalizeAssistantFlowNodes(
|
||||
raw: unknown,
|
||||
nodeFamily: 'block' | 'container'
|
||||
): AssistantDraftPayload['flow']['blocks'] | AssistantDraftPayload['flow']['containers'] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
|
||||
return raw
|
||||
.filter((node): node is Record<string, unknown> =>
|
||||
!!node && typeof node === 'object' && !Array.isArray(node)
|
||||
)
|
||||
.map((node) => ({
|
||||
...node,
|
||||
nodeFamily,
|
||||
specificConfiguration: normalizeAssistantFlowValue(node['specificConfiguration'])
|
||||
})) as AssistantDraftPayload['flow']['blocks'] | AssistantDraftPayload['flow']['containers'];
|
||||
}
|
||||
|
||||
function normalizeAssistantFlowValue(raw: unknown): unknown {
|
||||
if (Array.isArray(raw)) {
|
||||
return raw.map((item) => normalizeAssistantFlowValue(item));
|
||||
}
|
||||
if (!raw || typeof raw !== 'object') return raw;
|
||||
|
||||
const value = raw as Record<string, unknown>;
|
||||
if (isAssistantFlowData(value)) {
|
||||
return mapAssistantFlowData(value);
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, item]) => [key, normalizeAssistantFlowValue(item)])
|
||||
);
|
||||
}
|
||||
|
||||
function isAssistantFlowData(value: Record<string, unknown>): boolean {
|
||||
return ['blocks', 'containers', 'connections'].every((key) =>
|
||||
Object.prototype.hasOwnProperty.call(value, key)
|
||||
&& (value[key] == null || Array.isArray(value[key]))
|
||||
);
|
||||
}
|
||||
|
||||
function mapValidationIssues(raw: unknown): AssistantValidationIssue[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
|
||||
|
|
|
|||
|
|
@ -440,6 +440,7 @@
|
|||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container-node__port-label--input {
|
||||
|
|
@ -475,6 +476,13 @@
|
|||
}
|
||||
|
||||
.container-node__port-context {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 9px;
|
||||
line-height: 1.1;
|
||||
font-weight: 700;
|
||||
|
|
@ -483,6 +491,13 @@
|
|||
}
|
||||
|
||||
.container-node__port-name {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@
|
|||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
<span class="container-node__port-label container-node__port-label--input">
|
||||
<span class="container-node__port-label container-node__port-label--input" [title]="inputDisplayLabel(input.key)">
|
||||
@if (inputDisplayLabelParts(input.key).context; as context) {
|
||||
<span class="container-node__port-context">{{ context }}</span>
|
||||
}
|
||||
|
|
@ -152,7 +152,7 @@
|
|||
<div class="container-node__port-title">Outputs</div>
|
||||
@for (output of outputs; track output.key) {
|
||||
<div class="container-node__port-row container-node__port-row--right">
|
||||
<span class="container-node__port-label container-node__port-label--output">
|
||||
<span class="container-node__port-label container-node__port-label--output" [title]="outputDisplayLabel(output.key)">
|
||||
@if (outputDisplayLabelParts(output.key).context; as context) {
|
||||
<span class="container-node__port-context">{{ context }}</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -580,6 +580,8 @@
|
|||
|
||||
.llm-pill {
|
||||
min-height: 32px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border-radius: 7px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 11px;
|
||||
|
|
@ -589,14 +591,18 @@
|
|||
align-items: center;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.llm-pill-meta {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.llm-pill-meta-output {
|
||||
|
|
@ -604,6 +610,9 @@
|
|||
}
|
||||
|
||||
.llm-pill-name {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
|
@ -624,7 +633,7 @@
|
|||
background: #ecfdf3;
|
||||
border-color: #bbf7d0;
|
||||
gap: 4px;
|
||||
overflow: visible;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.llm-node--human .llm-pill-input {
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@
|
|||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
<span class="llm-pill llm-pill-input">
|
||||
<span class="llm-pill llm-pill-input" [title]="inputDisplayLabel(input.key)">
|
||||
<span class="llm-pill-meta">
|
||||
<span class="llm-pill-name">{{ inputDisplayLabel(input.key) }}</span>
|
||||
@if (!isReadonly && canTogglePortMultiplicity('input', input.key)) {
|
||||
|
|
@ -167,7 +167,7 @@
|
|||
<div class="llm-column-title llm-column-title-right">{{ outputsTitle() }}</div>
|
||||
@for (output of outputs; track output.key) {
|
||||
<div class="llm-row llm-row-output">
|
||||
<span class="llm-pill llm-pill-output" [ngClass]="outputPillClass(output.key)">
|
||||
<span class="llm-pill llm-pill-output" [ngClass]="outputPillClass(output.key)" [title]="outputDisplayLabel(output.key)">
|
||||
<span class="llm-pill-meta llm-pill-meta-output">
|
||||
<span class="llm-pill-name">{{ outputDisplayLabel(output.key) }}</span>
|
||||
@if (!isReadonly && canTogglePortMultiplicity('output', output.key)) {
|
||||
|
|
|
|||
|
|
@ -153,6 +153,27 @@ describe('GenericNodeComponent', () => {
|
|||
expect(outputLabels).toEqual(['noAssessment', 'excluded', 'continued']);
|
||||
});
|
||||
|
||||
it('keeps long port labels available as tooltips', () => {
|
||||
const longInput = 'input.with.a.very.long.descriptive.label.that.must.not.expand.the.node';
|
||||
const longOutput = 'output.with.a.very.long.descriptive.label.that.must.not.expand.the.node';
|
||||
fixture.componentRef.setInput('data', {
|
||||
...component.data,
|
||||
inputs: { [longInput]: { socket: { name: 'TEXT' } } },
|
||||
outputs: { [longOutput]: { socket: { name: 'TEXT' } } },
|
||||
data: {
|
||||
...component.data.data,
|
||||
inputs: [{ name: longInput, type: 'TEXT', multiple: false }],
|
||||
outputs: [{ name: longOutput, type: 'TEXT', multiple: false }]
|
||||
}
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const host = fixture.nativeElement as HTMLElement;
|
||||
expect(host.querySelector('.llm-row-input .llm-pill')?.getAttribute('title')).toBe(longInput);
|
||||
expect(host.querySelector('.llm-row-output .llm-pill')?.getAttribute('title')).toBe(longOutput);
|
||||
});
|
||||
|
||||
it('keeps a flexible port kind-selectable after narrowing it once', () => {
|
||||
fixture.componentRef.setInput('data', {
|
||||
...component.data,
|
||||
|
|
|
|||
Loading…
Reference in New Issue