Say what each block is in the palette, from its role
The palette knew two kinds of block, a human step and an automated one, so a Storage node, an End and a routing block all read 'Automated step'. It now goes by the role the block's type declares: a resource that is not a step, an end of a path, an automated or a human decision, a rejoin of branches, a human step, an automated step - each with its own icon. The server's BRANCH_REJOIN role is read as the web's MERGE, which it had never matched, so a rejoin was taken for an ordinary activity. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
672f53d9ab
commit
a8b1920fdb
|
|
@ -148,6 +148,7 @@ describe('toNodeCapabilities', () => {
|
|||
it('reads that a type routes exclusively, which is what lets it guard a loop', () => {
|
||||
expect(toNodeCapabilities({ visualRole: 'DECISION', routesExclusively: true }).routesExclusively).toBe(true);
|
||||
expect(toNodeCapabilities({ visualRole: 'RESOURCE' }).visualRole).toBe('RESOURCE');
|
||||
expect(toNodeCapabilities({ visualRole: 'BRANCH_REJOIN' }).visualRole).toBe('MERGE');
|
||||
});
|
||||
|
||||
it('falls back per-field for missing booleans and an unknown visualRole', () => {
|
||||
|
|
|
|||
|
|
@ -96,7 +96,9 @@ export function toPorts(
|
|||
const NODE_VISUAL_ROLES: NodeVisualRole[] = ['ACTIVITY', 'DECISION', 'MERGE', 'END', 'CONTAINER', 'RESOURCE'];
|
||||
|
||||
function toVisualRole(value: unknown): NodeVisualRole {
|
||||
const candidate = typeof value === 'string' ? value.toUpperCase() : '';
|
||||
const raw = typeof value === 'string' ? value.toUpperCase() : '';
|
||||
// The server calls a rejoin of branches BRANCH_REJOIN; the web has always called it MERGE.
|
||||
const candidate = raw === 'BRANCH_REJOIN' ? 'MERGE' : raw;
|
||||
return (NODE_VISUAL_ROLES as string[]).includes(candidate)
|
||||
? candidate as NodeVisualRole
|
||||
: DEFAULT_NODE_CAPABILITIES.visualRole;
|
||||
|
|
|
|||
|
|
@ -107,6 +107,19 @@
|
|||
color: #0ea5e9;
|
||||
}
|
||||
|
||||
.blocks-list-card-kind--resource {
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.blocks-list-card-kind--end {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.blocks-list-card-kind--decision,
|
||||
.blocks-list-card-kind--merge {
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.blocks-list-card-kind--container {
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,16 +33,14 @@
|
|||
(dragstart)="onDragStart($event, block)">
|
||||
|
||||
<div class="blocks-list-card-row">
|
||||
@let kind = kindOf(block);
|
||||
<mat-icon
|
||||
class="blocks-list-card-kind"
|
||||
[class.blocks-list-card-kind--interactive]="block.userInteractive"
|
||||
[fontIcon]="block.userInteractive ? 'touch_app' : 'auto_awesome'"></mat-icon>
|
||||
[class]="'blocks-list-card-kind blocks-list-card-kind--' + kind.modifier"
|
||||
[fontIcon]="kind.icon"></mat-icon>
|
||||
|
||||
<div class="blocks-list-card-meta">
|
||||
<div class="blocks-list-card-title">{{ block.type }}</div>
|
||||
<div class="blocks-list-card-sub">
|
||||
{{ block.userInteractive ? 'Human step' : 'Automated step' }}
|
||||
</div>
|
||||
<div class="blocks-list-card-sub">{{ kind.label }}</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
|
||||
|
||||
import { DEFAULT_NODE_CAPABILITIES } from '@models/flow';
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BlocksService } from '@services/blocks/blocks';
|
||||
import { ListState } from '@stores/list-state';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { BlocksList } from './blocks-list';
|
||||
import { blockKind, BlocksList } from './blocks-list';
|
||||
|
||||
const BLOCK_TYPES = [
|
||||
{
|
||||
|
|
@ -82,4 +83,16 @@ describe('BlocksList', () => {
|
|||
|
||||
expect(component.infoOpenFor()).toBe('SomethingElse');
|
||||
});
|
||||
|
||||
it('says what each block is from the role its type declares, not only whether a person is in it', () => {
|
||||
const base = { ...DEFAULT_NODE_CAPABILITIES };
|
||||
expect(blockKind({ userInteractive: false, capabilities: { ...base, visualRole: 'RESOURCE' } }).label).toBe('Resource · not a step');
|
||||
expect(blockKind({ userInteractive: false, capabilities: { ...base, visualRole: 'END', terminal: true } }).label).toBe('End of a path');
|
||||
expect(blockKind({ userInteractive: false, capabilities: { ...base, visualRole: 'DECISION', routesExclusively: true } }).label).toBe('Automated decision');
|
||||
expect(blockKind({ userInteractive: true, capabilities: { ...base, visualRole: 'DECISION', routesExclusively: true } }).label).toBe('Human decision');
|
||||
expect(blockKind({ userInteractive: true, capabilities: base }).label).toBe('Human step');
|
||||
expect(blockKind({ userInteractive: false, capabilities: { ...base, visualRole: 'MERGE' } }).label).toBe('Branches rejoin');
|
||||
expect(blockKind({ userInteractive: false, capabilities: base }).label).toBe('Automated step');
|
||||
expect(blockKind({ userInteractive: false }).label).toBe('Automated step');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,6 +14,28 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { BLOCK_TYPE_DRAG_MIME } from './block-drag';
|
||||
|
||||
/** What a block is in a flow, as the palette says it: its icon, its words, its colour. */
|
||||
export type BlockKind = { icon: string; label: string; modifier: string };
|
||||
|
||||
/**
|
||||
* From the role its type declares, not only whether a person is involved: a storage is not a step
|
||||
* at all, an end closes a path, a decision picks one branch - none of them is "an automated step".
|
||||
*/
|
||||
export function blockKind(block: Pick<BlockType, 'userInteractive' | 'capabilities'>): BlockKind {
|
||||
const capabilities = block.capabilities;
|
||||
const decides = capabilities?.visualRole === 'DECISION' || capabilities?.routesExclusively === true;
|
||||
if (capabilities?.visualRole === 'RESOURCE') return { icon: 'database', label: 'Resource · not a step', modifier: 'resource' };
|
||||
if (block.userInteractive) {
|
||||
return decides
|
||||
? { icon: 'touch_app', label: 'Human decision', modifier: 'interactive' }
|
||||
: { icon: 'touch_app', label: 'Human step', modifier: 'interactive' };
|
||||
}
|
||||
if (capabilities?.visualRole === 'END' || capabilities?.terminal === true) return { icon: 'flag', label: 'End of a path', modifier: 'end' };
|
||||
if (decides) return { icon: 'call_split', label: 'Automated decision', modifier: 'decision' };
|
||||
if (capabilities?.visualRole === 'MERGE') return { icon: 'merge', label: 'Branches rejoin', modifier: 'merge' };
|
||||
return { icon: 'auto_awesome', label: 'Automated step', modifier: 'automated' };
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-blocks-list',
|
||||
imports: [MatCardModule, MatFormFieldModule, MatIconModule, MatInputModule, MatProgressSpinnerModule, MatTooltipModule],
|
||||
|
|
@ -93,6 +115,10 @@ export class BlocksList extends ListStateViewHolder<BlockType> {
|
|||
|
||||
readonly showLoading = computed(() => this.loading() || this.serviceLoading());
|
||||
|
||||
kindOf(block: BlockType): BlockKind {
|
||||
return blockKind(block);
|
||||
}
|
||||
|
||||
onDragStart(event: DragEvent, block: BlockType) {
|
||||
if (!event.dataTransfer) return;
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
|
|
|
|||
Loading…
Reference in New Issue