fix(agent-graph): tune stable slot spacing
This commit is contained in:
parent
937b23be4b
commit
ade312ad87
6 changed files with 163 additions and 52 deletions
|
|
@ -1,7 +1,8 @@
|
||||||
export const STABLE_SLOT_GEOMETRY = {
|
export const STABLE_SLOT_GEOMETRY = {
|
||||||
slotVerticalGap: 24,
|
slotVerticalGap: 12,
|
||||||
slotHorizontalGap: 32,
|
slotHorizontalGap: 77.7,
|
||||||
ringGap: 140,
|
ringGap: 140,
|
||||||
|
centralHorizontalGap: 77.7,
|
||||||
centralSafetyPadding: 48,
|
centralSafetyPadding: 48,
|
||||||
memberSlotInnerPadding: 16,
|
memberSlotInnerPadding: 16,
|
||||||
centralBlockGap: 56,
|
centralBlockGap: 56,
|
||||||
|
|
@ -9,7 +10,7 @@ export const STABLE_SLOT_GEOMETRY = {
|
||||||
unassignedGap: 72,
|
unassignedGap: 72,
|
||||||
maxGeneratedRings: 12,
|
maxGeneratedRings: 12,
|
||||||
ownerCollisionPadding: 28,
|
ownerCollisionPadding: 28,
|
||||||
ownerBandHeight: 96,
|
ownerBandHeight: 32,
|
||||||
ownerMinWidth: 200,
|
ownerMinWidth: 200,
|
||||||
processBandHeight: 32,
|
processBandHeight: 32,
|
||||||
processRailWidth: 220,
|
processRailWidth: 220,
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ const PROCESS_RAIL_NODE_GAP = 42;
|
||||||
const PROCESS_RAIL_NODE_FOOTPRINT = 28;
|
const PROCESS_RAIL_NODE_FOOTPRINT = 28;
|
||||||
const GEOMETRY_EPSILON = 0.001;
|
const GEOMETRY_EPSILON = 0.001;
|
||||||
const SMALL_TEAM_CARDINAL_RADIUS_STEP = 24;
|
const SMALL_TEAM_CARDINAL_RADIUS_STEP = 24;
|
||||||
|
const SMALL_TEAM_CARDINAL_VERTICAL_PADDING = 77.7;
|
||||||
|
|
||||||
const SECTOR_VECTORS = STABLE_SLOT_SECTOR_VECTORS;
|
const SECTOR_VECTORS = STABLE_SLOT_SECTOR_VECTORS;
|
||||||
const SMALL_TEAM_CARDINAL_LAYOUTS: ReadonlyArray<
|
const SMALL_TEAM_CARDINAL_LAYOUTS: ReadonlyArray<
|
||||||
|
|
@ -265,7 +266,9 @@ function rectOverlapsAnyCentralRect(
|
||||||
rect: StableRect,
|
rect: StableRect,
|
||||||
centralCollisionRects: readonly StableRect[]
|
centralCollisionRects: readonly StableRect[]
|
||||||
): boolean {
|
): boolean {
|
||||||
return centralCollisionRects.some((centralRect) => rectsOverlap(rect, centralRect));
|
return centralCollisionRects.some((centralRect) =>
|
||||||
|
rectsOverlapWithAxisGap(rect, centralRect, SLOT_GEOMETRY.centralHorizontalGap, 0)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeOwnerFootprints(
|
export function computeOwnerFootprints(
|
||||||
|
|
@ -988,21 +991,25 @@ function planStrictSmallTeamOwnerSlots(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let radius = Math.max(
|
const baseRadiusByAxis = resolveStrictSmallTeamRadiusByAxis(
|
||||||
...slotConfigs.map((slot) =>
|
slotConfigs.map((slot) => slot!),
|
||||||
resolveMinimumDirectionalRadiusForVector({
|
centralCollisionRects,
|
||||||
vector: slot!.vector,
|
runtimeCentralExclusion
|
||||||
footprint: slot!.footprint,
|
|
||||||
centralCollisionRects,
|
|
||||||
runtimeCentralExclusion,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
for (let iteration = 0; iteration < 48; iteration += 1) {
|
for (let iteration = 0; iteration < 48; iteration += 1) {
|
||||||
const frames = slotConfigs.map((slot) =>
|
const radiusBump = iteration * SMALL_TEAM_CARDINAL_RADIUS_STEP;
|
||||||
buildSlotFrameAtRadiusWithVector(slot!.footprint, slot!.assignment, radius, slot!.vector)
|
const frames = slotConfigs.map((slot) => {
|
||||||
);
|
const axis = resolveStrictSmallTeamVectorAxis(slot!.vector);
|
||||||
|
return buildSlotFrameAtRadiusWithVector(
|
||||||
|
slot!.footprint,
|
||||||
|
slot!.assignment,
|
||||||
|
baseRadiusByAxis[axis] +
|
||||||
|
(axis === 'vertical' ? SMALL_TEAM_CARDINAL_VERTICAL_PADDING : 0) +
|
||||||
|
radiusBump,
|
||||||
|
slot!.vector
|
||||||
|
);
|
||||||
|
});
|
||||||
const allValid = frames.every((frame, frameIndex) =>
|
const allValid = frames.every((frame, frameIndex) =>
|
||||||
isSlotFramePlacementValid(
|
isSlotFramePlacementValid(
|
||||||
frame,
|
frame,
|
||||||
|
|
@ -1013,12 +1020,42 @@ function planStrictSmallTeamOwnerSlots(
|
||||||
if (allValid) {
|
if (allValid) {
|
||||||
return frames;
|
return frames;
|
||||||
}
|
}
|
||||||
radius += SMALL_TEAM_CARDINAL_RADIUS_STEP;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveStrictSmallTeamRadiusByAxis(
|
||||||
|
slotConfigs: readonly {
|
||||||
|
footprint: OwnerFootprint;
|
||||||
|
vector: { x: number; y: number };
|
||||||
|
}[],
|
||||||
|
centralCollisionRects: readonly StableRect[],
|
||||||
|
runtimeCentralExclusion: StableRect
|
||||||
|
): Record<'horizontal' | 'vertical', number> {
|
||||||
|
const radiusByAxis = {
|
||||||
|
horizontal: 0,
|
||||||
|
vertical: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const slot of slotConfigs) {
|
||||||
|
const axis = resolveStrictSmallTeamVectorAxis(slot.vector);
|
||||||
|
const radius = resolveMinimumDirectionalRadiusForVector({
|
||||||
|
vector: slot.vector,
|
||||||
|
footprint: slot.footprint,
|
||||||
|
centralCollisionRects,
|
||||||
|
runtimeCentralExclusion,
|
||||||
|
});
|
||||||
|
radiusByAxis[axis] = Math.max(radiusByAxis[axis], radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
return radiusByAxis;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveStrictSmallTeamVectorAxis(vector: { x: number; y: number }): 'horizontal' | 'vertical' {
|
||||||
|
return Math.abs(vector.x) >= Math.abs(vector.y) ? 'horizontal' : 'vertical';
|
||||||
|
}
|
||||||
|
|
||||||
function buildPreferredAssignmentsMap(
|
function buildPreferredAssignmentsMap(
|
||||||
assignments?: Record<string, GraphOwnerSlotAssignment>
|
assignments?: Record<string, GraphOwnerSlotAssignment>
|
||||||
): Map<string, GraphOwnerSlotAssignment> {
|
): Map<string, GraphOwnerSlotAssignment> {
|
||||||
|
|
@ -1329,7 +1366,7 @@ function rankNearestSlotAssignmentResult(args: {
|
||||||
if (
|
if (
|
||||||
!isSlotFramePlacementValid(frame, otherFrames, centralCollisionRects) ||
|
!isSlotFramePlacementValid(frame, otherFrames, centralCollisionRects) ||
|
||||||
!isSlotFramePlacementValid(displacedFrame, otherFrames, centralCollisionRects) ||
|
!isSlotFramePlacementValid(displacedFrame, otherFrames, centralCollisionRects) ||
|
||||||
rectsOverlapWithGap(frame.bounds, displacedFrame.bounds, SLOT_GEOMETRY.ringPadding)
|
ownerSlotFramesOverlap(frame.bounds, displacedFrame.bounds)
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -1735,12 +1772,17 @@ function resolveTaskColumnKey(task: GraphNode): string {
|
||||||
return 'todo';
|
return 'todo';
|
||||||
}
|
}
|
||||||
|
|
||||||
function rectsOverlapWithGap(a: StableRect, b: StableRect, gap: number): boolean {
|
function rectsOverlapWithAxisGap(
|
||||||
|
a: StableRect,
|
||||||
|
b: StableRect,
|
||||||
|
horizontalGap: number,
|
||||||
|
verticalGap: number
|
||||||
|
): boolean {
|
||||||
return (
|
return (
|
||||||
a.left - gap < b.right &&
|
a.left - horizontalGap < b.right &&
|
||||||
a.right + gap > b.left &&
|
a.right + horizontalGap > b.left &&
|
||||||
a.top - gap < b.bottom &&
|
a.top - verticalGap < b.bottom &&
|
||||||
a.bottom + gap > b.top
|
a.bottom + verticalGap > b.top
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1748,6 +1790,15 @@ function rectsOverlap(a: StableRect, b: StableRect): boolean {
|
||||||
return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
|
return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ownerSlotFramesOverlap(a: StableRect, b: StableRect): boolean {
|
||||||
|
return rectsOverlapWithAxisGap(
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
SLOT_GEOMETRY.slotHorizontalGap,
|
||||||
|
SLOT_GEOMETRY.ringPadding
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function rectContainsRect(outer: StableRect, inner: StableRect): boolean {
|
function rectContainsRect(outer: StableRect, inner: StableRect): boolean {
|
||||||
return (
|
return (
|
||||||
inner.left >= outer.left - GEOMETRY_EPSILON &&
|
inner.left >= outer.left - GEOMETRY_EPSILON &&
|
||||||
|
|
@ -1788,9 +1839,7 @@ function isSlotFramePlacementValid(
|
||||||
if (rectOverlapsAnyCentralRect(frame.bounds, centralCollisionRects)) {
|
if (rectOverlapsAnyCentralRect(frame.bounds, centralCollisionRects)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !existingFrames.some((existing) =>
|
return !existingFrames.some((existing) => ownerSlotFramesOverlap(frame.bounds, existing.bounds));
|
||||||
rectsOverlapWithGap(frame.bounds, existing.bounds, SLOT_GEOMETRY.ringPadding)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAssignmentKey(assignment: GraphOwnerSlotAssignment): string {
|
function buildAssignmentKey(assignment: GraphOwnerSlotAssignment): string {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import { useLayoutEffect } from 'react';
|
|
||||||
|
|
||||||
import { useStore } from '@renderer/store';
|
|
||||||
import { isTeamGraphSlotPersistenceDisabled } from '@renderer/store/slices/teamSlice';
|
|
||||||
|
|
||||||
export function useTeamGraphSlotReset(teamName: string, enabled = true): void {
|
|
||||||
const resetTeamGraphSlotAssignmentsToDefaults = useStore(
|
|
||||||
(s) => s.resetTeamGraphSlotAssignmentsToDefaults
|
|
||||||
);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!enabled || !isTeamGraphSlotPersistenceDisabled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resetTeamGraphSlotAssignmentsToDefaults(teamName);
|
|
||||||
}, [enabled, resetTeamGraphSlotAssignmentsToDefaults, teamName]);
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,6 @@ import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHo
|
||||||
import { useGraphCreateTaskDialog } from '../hooks/useGraphCreateTaskDialog';
|
import { useGraphCreateTaskDialog } from '../hooks/useGraphCreateTaskDialog';
|
||||||
import { useGraphSidebarVisibility } from '../hooks/useGraphSidebarVisibility';
|
import { useGraphSidebarVisibility } from '../hooks/useGraphSidebarVisibility';
|
||||||
import { useTeamGraphAdapter } from '../hooks/useTeamGraphAdapter';
|
import { useTeamGraphAdapter } from '../hooks/useTeamGraphAdapter';
|
||||||
import { useTeamGraphSlotReset } from '../hooks/useTeamGraphSlotReset';
|
|
||||||
import { useTeamGraphSurfaceActions } from '../hooks/useTeamGraphSurfaceActions';
|
import { useTeamGraphSurfaceActions } from '../hooks/useTeamGraphSurfaceActions';
|
||||||
|
|
||||||
import { GraphActivityHud } from './GraphActivityHud';
|
import { GraphActivityHud } from './GraphActivityHud';
|
||||||
|
|
@ -61,8 +60,6 @@ export const TeamGraphOverlay = ({
|
||||||
const effectiveSidebarVisible = sidebarVisible ?? persistedSidebarVisible;
|
const effectiveSidebarVisible = sidebarVisible ?? persistedSidebarVisible;
|
||||||
const handleToggleSidebar = onToggleSidebar ?? toggleSidebarVisible;
|
const handleToggleSidebar = onToggleSidebar ?? toggleSidebarVisible;
|
||||||
|
|
||||||
useTeamGraphSlotReset(teamName);
|
|
||||||
|
|
||||||
// Task action dispatchers (same pattern as TeamGraphTab)
|
// Task action dispatchers (same pattern as TeamGraphTab)
|
||||||
const dispatchTaskAction = useCallback(
|
const dispatchTaskAction = useCallback(
|
||||||
(action: string) => (taskId: string) =>
|
(action: string) => (taskId: string) =>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHo
|
||||||
import { useGraphCreateTaskDialog } from '../hooks/useGraphCreateTaskDialog';
|
import { useGraphCreateTaskDialog } from '../hooks/useGraphCreateTaskDialog';
|
||||||
import { useGraphSidebarVisibility } from '../hooks/useGraphSidebarVisibility';
|
import { useGraphSidebarVisibility } from '../hooks/useGraphSidebarVisibility';
|
||||||
import { useTeamGraphAdapter } from '../hooks/useTeamGraphAdapter';
|
import { useTeamGraphAdapter } from '../hooks/useTeamGraphAdapter';
|
||||||
import { useTeamGraphSlotReset } from '../hooks/useTeamGraphSlotReset';
|
|
||||||
import { useTeamGraphSurfaceActions } from '../hooks/useTeamGraphSurfaceActions';
|
import { useTeamGraphSurfaceActions } from '../hooks/useTeamGraphSurfaceActions';
|
||||||
|
|
||||||
import { GraphActivityHud } from './GraphActivityHud';
|
import { GraphActivityHud } from './GraphActivityHud';
|
||||||
|
|
@ -52,8 +51,6 @@ export const TeamGraphTab = ({
|
||||||
const { sidebarVisible, toggleSidebarVisible } = useGraphSidebarVisibility();
|
const { sidebarVisible, toggleSidebarVisible } = useGraphSidebarVisibility();
|
||||||
const { dialog: createTaskDialog, openCreateTaskDialog } = useGraphCreateTaskDialog(teamName);
|
const { dialog: createTaskDialog, openCreateTaskDialog } = useGraphCreateTaskDialog(teamName);
|
||||||
|
|
||||||
useTeamGraphSlotReset(teamName, isActive);
|
|
||||||
|
|
||||||
// Typed event dispatchers (DRY — used in both events + renderOverlay)
|
// Typed event dispatchers (DRY — used in both events + renderOverlay)
|
||||||
const dispatchOpenTask = useCallback(
|
const dispatchOpenTask = useCallback(
|
||||||
(taskId: string) =>
|
(taskId: string) =>
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,26 @@ function rectsOverlap(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rectsOverlapVertically(
|
||||||
|
left: { top: number; bottom: number },
|
||||||
|
right: { top: number; bottom: number }
|
||||||
|
): boolean {
|
||||||
|
return left.top < right.bottom && left.bottom > right.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
function horizontalGapBetween(
|
||||||
|
left: { left: number; right: number },
|
||||||
|
right: { left: number; right: number }
|
||||||
|
): number {
|
||||||
|
if (left.right <= right.left) {
|
||||||
|
return right.left - left.right;
|
||||||
|
}
|
||||||
|
if (right.right <= left.left) {
|
||||||
|
return left.left - right.right;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
describe('stable slot layout planner', () => {
|
describe('stable slot layout planner', () => {
|
||||||
it('does not build a stable slot snapshot when the lead is missing', () => {
|
it('does not build a stable slot snapshot when the lead is missing', () => {
|
||||||
const snapshot = buildStableSlotLayoutSnapshot({
|
const snapshot = buildStableSlotLayoutSnapshot({
|
||||||
|
|
@ -153,6 +173,7 @@ describe('stable slot layout planner', () => {
|
||||||
expect(frame?.activityColumnRect.left).toBe(frame?.boardBandRect.left);
|
expect(frame?.activityColumnRect.left).toBe(frame?.boardBandRect.left);
|
||||||
expect(frame?.kanbanBandRect.left).toBeGreaterThan(frame?.activityColumnRect.right ?? 0);
|
expect(frame?.kanbanBandRect.left).toBeGreaterThan(frame?.activityColumnRect.right ?? 0);
|
||||||
expect(frame?.processBandRect.width).toBe(computeProcessBandWidth(0));
|
expect(frame?.processBandRect.width).toBe(computeProcessBandWidth(0));
|
||||||
|
expect(frame?.processBandRect.height).toBe(STABLE_SLOT_GEOMETRY.processBandHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses strict cardinal owner slots for teams with up to four members', () => {
|
it('uses strict cardinal owner slots for teams with up to four members', () => {
|
||||||
|
|
@ -200,6 +221,7 @@ describe('stable slot layout planner', () => {
|
||||||
|
|
||||||
expect(Math.abs(Math.abs(leftFrame.ownerX) - Math.abs(rightFrame.ownerX))).toBeLessThan(1);
|
expect(Math.abs(Math.abs(leftFrame.ownerX) - Math.abs(rightFrame.ownerX))).toBeLessThan(1);
|
||||||
expect(Math.abs(Math.abs(topFrame.ownerY) - Math.abs(bottomFrame.ownerY))).toBeLessThan(1);
|
expect(Math.abs(Math.abs(topFrame.ownerY) - Math.abs(bottomFrame.ownerY))).toBeLessThan(1);
|
||||||
|
expect(Math.abs(topFrame.ownerY)).toBeLessThan(Math.abs(rightFrame.ownerX));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses strict cardinal owner slots even when ownerOrder differs from assignment order', () => {
|
it('uses strict cardinal owner slots even when ownerOrder differs from assignment order', () => {
|
||||||
|
|
@ -246,6 +268,67 @@ describe('stable slot layout planner', () => {
|
||||||
expect(Math.abs(jackFrame.ownerY)).toBeLessThan(1);
|
expect(Math.abs(jackFrame.ownerY)).toBeLessThan(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps horizontal spacing around lead columns and between side-by-side owners', () => {
|
||||||
|
const teamName = 'team-horizontal-spacing';
|
||||||
|
const lead = createLead(teamName);
|
||||||
|
const members = [
|
||||||
|
createMember(teamName, 'agent-top', 'top'),
|
||||||
|
createMember(teamName, 'agent-right', 'right'),
|
||||||
|
createMember(teamName, 'agent-bottom', 'bottom'),
|
||||||
|
createMember(teamName, 'agent-left', 'left'),
|
||||||
|
createMember(teamName, 'agent-top-right', 'top-right'),
|
||||||
|
createMember(teamName, 'agent-bottom-right', 'bottom-right'),
|
||||||
|
];
|
||||||
|
const tasks = [
|
||||||
|
createTask(teamName, 'lead-todo', lead.id),
|
||||||
|
createTask(teamName, 'lead-wip', lead.id, { taskStatus: 'in_progress' }),
|
||||||
|
...members.map((member, index) => createTask(teamName, `task-${index + 1}`, member.id)),
|
||||||
|
];
|
||||||
|
const layout: GraphLayoutPort = {
|
||||||
|
version: 'stable-slots-v1',
|
||||||
|
ownerOrder: members.map((member) => member.id),
|
||||||
|
slotAssignments: {
|
||||||
|
[members[0]!.id]: { ringIndex: 0, sectorIndex: 0 },
|
||||||
|
[members[1]!.id]: { ringIndex: 0, sectorIndex: 1 },
|
||||||
|
[members[2]!.id]: { ringIndex: 0, sectorIndex: 2 },
|
||||||
|
[members[3]!.id]: { ringIndex: 0, sectorIndex: 3 },
|
||||||
|
[members[4]!.id]: { ringIndex: 0, sectorIndex: 4 },
|
||||||
|
[members[5]!.id]: { ringIndex: 0, sectorIndex: 5 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const snapshot = buildStableSlotLayoutSnapshot({
|
||||||
|
teamName,
|
||||||
|
nodes: [lead, ...members, ...tasks],
|
||||||
|
layout,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot).not.toBeNull();
|
||||||
|
expect(validateStableSlotLayout(snapshot!)).toEqual({ valid: true });
|
||||||
|
|
||||||
|
for (const frame of snapshot!.memberSlotFrames) {
|
||||||
|
for (const centralRect of snapshot!.centralCollisionRects) {
|
||||||
|
if (!rectsOverlapVertically(frame.bounds, centralRect)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
expect(horizontalGapBetween(frame.bounds, centralRect)).toBeGreaterThanOrEqual(
|
||||||
|
STABLE_SLOT_GEOMETRY.centralHorizontalGap
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [index, left] of snapshot!.memberSlotFrames.entries()) {
|
||||||
|
for (const right of snapshot!.memberSlotFrames.slice(index + 1)) {
|
||||||
|
if (!rectsOverlapVertically(left.bounds, right.bounds)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
expect(horizontalGapBetween(left.bounds, right.bounds)).toBeGreaterThanOrEqual(
|
||||||
|
STABLE_SLOT_GEOMETRY.slotHorizontalGap
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('reserves a full empty activity column and minimum kanban width for idle members', () => {
|
it('reserves a full empty activity column and minimum kanban width for idle members', () => {
|
||||||
const teamName = 'team-empty-slot';
|
const teamName = 'team-empty-slot';
|
||||||
const lead = createLead(teamName);
|
const lead = createLead(teamName);
|
||||||
|
|
@ -825,8 +908,10 @@ describe('stable slot layout planner', () => {
|
||||||
expect(snapshot!.centralCollisionRects).toContain(snapshot!.leadSlotFrame.processBandRect);
|
expect(snapshot!.centralCollisionRects).toContain(snapshot!.leadSlotFrame.processBandRect);
|
||||||
expect(snapshot!.centralCollisionRects).toContain(snapshot!.leadSlotFrame.activityColumnRect);
|
expect(snapshot!.centralCollisionRects).toContain(snapshot!.leadSlotFrame.activityColumnRect);
|
||||||
expect(snapshot!.centralCollisionRects).toContain(snapshot!.leadSlotFrame.kanbanBandRect);
|
expect(snapshot!.centralCollisionRects).toContain(snapshot!.leadSlotFrame.kanbanBandRect);
|
||||||
expect(snapshot!.leadCentralReservedBlock.width).toBeLessThan(snapshot!.leadSlotFrame.bounds.width);
|
expect(snapshot!.leadCentralReservedBlock.width).toBeLessThan(
|
||||||
expect(snapshot!.leadCentralReservedBlock.height).toBeLessThan(
|
snapshot!.leadSlotFrame.bounds.width
|
||||||
|
);
|
||||||
|
expect(snapshot!.leadCentralReservedBlock.height).toBeLessThanOrEqual(
|
||||||
snapshot!.leadSlotFrame.bounds.height
|
snapshot!.leadSlotFrame.bounds.height
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue