Collapse the relationships card by default, with an honest count

The triage band answers the common case above the fold, so the full edge list is
reference rather than the first thing to read. Same caret and keyboard handling
as the collapsible panels on the same page.

The card stays: 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
collapse to two chips and which machine DEFAULTS to it is lost.

The header count is of RENDERED items, not raw edges: a symmetric Dualpath link
is stored in both directions, so counting edges read 3 above a card showing 2.

Frontend 280 passed.
This commit is contained in:
2026-08-29 19:52:20 -04:00
parent dd7dd435c0
commit 330fd31524

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;