Collapse the relationships card by default, with an honest count
All checks were successful
CI / naming (push) Successful in 4s
CI / frontend (push) Successful in 12s
CI / migrations-mysql (push) Successful in 41s
CI / backend (push) Successful in 4m7s

The triage band now answers the common case above the fold - what an asset
serves, or what drives it - so the full edge list below is reference rather than
the first thing to read. On a machine showing two relationships, both already
appeared in the band and the banner, and the card was a third copy.

Collapsed by default, using the same caret, roles and keyboard handling as the
collapsible panels on the same page rather than a second, slightly different
one.

The card is NOT redundant with the band and is not removed. It is the only
surface with Add Relationship and the per-row delete, and it keeps each edge's
exact type where the band deduplicates by asset. The printer page is the proof:
four rows there - usesprinter and defaultprinter, for each of two machines -
collapse to two band chips, and which machine DEFAULTS to that printer is a
distinction the band deliberately drops. Fine for triage, wrong for management.

THE COUNT IS OF WHAT IS RENDERED, NOT OF EDGES

A collapsed card that just says "Relationships" cannot be told from an empty
one, so the header carries a count. The first version counted raw edges and was
wrong on the first machine it was shown: a symmetric Dualpath link is stored in
BOTH directions in the legacy shape, so it is two rows in outgoing+incoming and
one "Connected" entry on screen. The badge read 3 above a card that shows 2 when
expanded - exactly the mismatch the badge exists to prevent. It now counts
rendered items.

Add Relationship expands the card before opening the modal, so a new row cannot
land behind a closed caret and read as the add having failed.

Verified on screen: machine reads 2 and matches, printer reads 4 and matches.
Frontend 280 passed.
This commit is contained in:
cproudlock
2026-08-29 19:52:03 -04:00
parent 78017eee2a
commit e562b405d4

View File

@@ -1,16 +1,32 @@
<template>
<div class="relationships-section">
<div class="section-header">
<h3 class="section-title">Relationships</h3>
<h3
class="section-title rel-header"
role="button"
tabindex="0"
:aria-expanded="!collapsed"
@click="collapsed = !collapsed"
@keydown.enter.prevent="collapsed = !collapsed"
@keydown.space.prevent="collapsed = !collapsed"
>
<span class="rel-caret" :class="{ collapsed }" aria-hidden="true">&#9662;</span>
<span>Relationships</span>
<!-- The count is what makes a collapsed card honest: without it, "no
relationships" and "four relationships you cannot see" look the
same from the outside. -->
<span v-if="!loading && relationshipCount" class="rel-count">{{ relationshipCount }}</span>
</h3>
<button
v-if="authStore.isAuthenticated"
class="btn btn-sm btn-primary"
@click="showAddModal = true"
@click.stop="openAdd"
>
Add Relationship
</button>
</div>
<div v-show="!collapsed">
<div v-if="loading" class="loading">Loading relationships...</div>
<div v-else-if="!hasRelationships" class="empty-state">
@@ -96,6 +112,7 @@
</div>
</div>
</template>
</div>
<!-- Add Relationship Modal -->
<div v-if="showAddModal" class="modal-overlay">
@@ -225,6 +242,22 @@ function colorForType(name) {
}
const showAddModal = ref(false)
// Collapsed by default. The triage band above answers the common case - what
// this asset serves or is driven by - so the full edge list is reference rather
// than the first thing to read. It is NOT redundant with the band and is not
// removed: it is the only surface with Add and delete, and it keeps each edge's
// exact type where the band deduplicates by asset. On a printer, four rows
// (usesprinter and defaultprinter, per machine) collapse to two band chips, and
// which machine DEFAULTS to it is a distinction the band deliberately drops.
const collapsed = ref(true)
function openAdd() {
// Adding from a collapsed card would otherwise drop the new row behind the
// caret, which reads as the add having failed.
collapsed.value = false
showAddModal.value = true
}
// New relationship form
const newRel = ref({
direction: 'outgoing',
@@ -305,6 +338,14 @@ const directionalItems = computed(() => {
return items
})
// Counts what the card RENDERS, not raw edges. A symmetric link (Dualpath) is
// stored in both directions in the legacy shape, so it is two rows in
// outgoing+incoming and one "Connected" entry on screen. Counting edges
// displayed 3 on a machine that shows 2 when you expand it, which is precisely
// the mismatch this badge exists to avoid.
const relationshipCount = computed(
() => connectedItems.value.length + directionalItems.value.length)
const canSave = computed(() => {
return newRel.value.relationshiptypeid && newRel.value.targetAssetId && resolvedAssetId.value
})
@@ -502,6 +543,38 @@ const getAssetRoute = assetRoute
font-weight: 600;
}
/* Matches the collapsible panels on the same page (PluginAssetPanels) so a
card does not gain a second, slightly different caret. */
.rel-header {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
user-select: none;
}
.rel-caret {
font-size: 0.7rem;
color: var(--text-light);
transition: transform 0.15s ease;
}
.rel-caret.collapsed { transform: rotate(-90deg); }
.rel-count {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-light);
background: var(--bg-hover, rgba(127, 127, 127, 0.14));
border-radius: 999px;
padding: 0.05rem 0.45rem;
font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce) {
.rel-caret { transition: none; }
}
.empty-state {
color: var(--text-light);
font-style: italic;