From 4a72f85e0315526b6aed80fcdebd53902ce94de1 Mon Sep 17 00:00:00 2001 From: Matt Dawkins Date: Mon, 28 Sep 2026 23:31:53 -0400 Subject: [PATCH 1/3] Add track checkboxes to the bottom list and fix its attribute editor Rows in the bottom track list get the checkbox the side list has. The attribute editor never took focus, so it stayed open; length columns are narrower. --- client/src/components/Tracks/TrackList.vue | 10 +++- .../bottombar/BottomBarTrackItemView.spec.ts | 44 ++++++++++++++- .../bottombar/BottomBarTrackItemView.vue | 56 ++++++++++++++++--- 3 files changed, 97 insertions(+), 13 deletions(-) diff --git a/client/src/components/Tracks/TrackList.vue b/client/src/components/Tracks/TrackList.vue index 4290ae0cc..3c5c1c8f6 100644 --- a/client/src/components/Tracks/TrackList.vue +++ b/client/src/components/Tracks/TrackList.vue @@ -600,8 +600,8 @@ export default defineComponent({ } } .col-spacer { - /* Matches color box: 10px + 6px margin */ - min-width: 16px; + /* Matches track-lead-compact (checkbox or color box): 24px + 6px margin */ + min-width: 30px; } .col-id { /* Matches trackNumber-compact: 30px + 8px margin */ @@ -643,6 +643,12 @@ export default defineComponent({ text-align: left; margin-right: 8px; } + .col-length { + /* Matches track-length: a number needs less room than free text */ + width: 64px; + min-width: 64px; + max-width: 64px; + } .col-conf, .col-length { position: relative; diff --git a/client/src/components/Tracks/bottombar/BottomBarTrackItemView.spec.ts b/client/src/components/Tracks/bottombar/BottomBarTrackItemView.spec.ts index 849e53101..b1802e538 100644 --- a/client/src/components/Tracks/bottombar/BottomBarTrackItemView.spec.ts +++ b/client/src/components/Tracks/bottombar/BottomBarTrackItemView.spec.ts @@ -1,4 +1,6 @@ -import { defineComponent, h, ref } from 'vue'; +import { + defineComponent, h, nextTick, ref, +} from 'vue'; import { shallowMount } from '@vue/test-utils'; import Track from '../../../track'; import BottomBarTrackItemView from './BottomBarTrackItemView.vue'; @@ -9,6 +11,7 @@ const providerState = vi.hoisted(() => ({ setTrackNotes: vi.fn(), setTrackAttribute: vi.fn(), setTrackFirstFeatureAttribute: vi.fn(), + updateCheckedId: vi.fn(), })); vi.mock('../../../provides', () => ({ @@ -17,6 +20,7 @@ vi.mock('../../../provides', () => ({ useTrackFilters: () => ({ allTypes: ref(['root', 'leaf']), hierarchyIndex: ref(undefined), + updateCheckedId: providerState.updateCheckedId, }), useCameraStore: () => ({ assignTrackType: providerState.assignTrackType, @@ -27,7 +31,7 @@ vi.mock('../../../provides', () => ({ }), })); -function mountItem(displayPairIndex: number) { +function mountItem(displayPairIndex: number, extra: Record = {}, attach = false) { const track = new Track(1, { begin: 0, end: 0, @@ -57,10 +61,14 @@ function mountItem(displayPairIndex: number) { toggleKeyframe: vi.fn(), toggleInterpolation: vi.fn(), toggleAllInterpolation: vi.fn(), + ...extra, }, }), }); - const wrapper = shallowMount(Host, { stubs: { BottomBarTrackItemView: false } }); + const wrapper = shallowMount(Host, { + stubs: { BottomBarTrackItemView: false }, + ...(attach ? { attachTo: document.body } : {}), + }); if (!child) { throw new Error('BottomBarTrackItemView did not mount'); } @@ -130,4 +138,34 @@ describe('BottomBarTrackItemView hierarchy display', () => { expect(providerState.setTrackFirstFeatureAttribute) .toHaveBeenCalledWith(1, 'occluded', 'yes'); }); + + it('shows a checkbox for the track, as the side list does, and a color box when solo', () => { + const { wrapper } = mountItem(0); + expect(wrapper.find('.type-color-box-compact').exists()).toBe(false); + wrapper.find('.track-checkbox-compact').trigger('change'); + expect(providerState.updateCheckedId.mock.calls[0][0]).toBe(1); + + const solo = mountItem(0, { solo: true }); + expect(solo.wrapper.find('.track-checkbox-compact').exists()).toBe(false); + expect(solo.wrapper.find('.type-color-box-compact').exists()).toBe(true); + }); + + it('focuses an attribute field when editing starts, so leaving it closes it', async () => { + const { wrapper, vm } = mountItem( + 0, + { columnVisibility: { attributeColumns: ['track_length'] } }, + true, + ); + expect(wrapper.find('.compact-attribute-input').exists()).toBe(false); + vm.startEditAttribute('track_length', new MouseEvent('click')); + await nextTick(); + await nextTick(); + const input = wrapper.find('.compact-attribute-input'); + expect(input.classes()).toContain('track-length'); + expect(document.activeElement).toBe(input.element); + + await input.trigger('blur'); + expect(wrapper.find('.compact-attribute-input').exists()).toBe(false); + wrapper.destroy(); + }); }); diff --git a/client/src/components/Tracks/bottombar/BottomBarTrackItemView.vue b/client/src/components/Tracks/bottombar/BottomBarTrackItemView.vue index bd4858f3d..a5cbe30f6 100644 --- a/client/src/components/Tracks/bottombar/BottomBarTrackItemView.vue +++ b/client/src/components/Tracks/bottombar/BottomBarTrackItemView.vue @@ -26,6 +26,8 @@ export default defineComponent({ fps: { type: Number, default: null }, editing: { type: Boolean, required: true }, inputValue: { type: Boolean, required: true }, + solo: { type: Boolean, default: false }, + disabled: { type: Boolean, default: false }, merging: { type: Boolean, default: false }, toggleKeyframe: { type: Function as PropType<() => void>, required: true }, toggleInterpolation: { type: Function as PropType<() => void>, required: true }, @@ -50,7 +52,8 @@ export default defineComponent({ const notesInputRef = ref(null); const editingAttributeKey = ref(null); const editAttributeValue = ref(''); - const attributeInputRef = ref(null); + // Rendered inside a v-for, where Vue collects the refs into an array + const attributeInputRef = ref(null); const localAttributeDisplay = ref>({}); watch(() => props.track.id, () => { @@ -209,8 +212,10 @@ export default defineComponent({ editAttributeValue.value = getAttributeValue(attrKey); editingAttributeKey.value = attrKey; nextTick(() => { - attributeInputRef.value?.focus(); - attributeInputRef.value?.select(); + // Without focus the field would never blur, and so never close + const input = [attributeInputRef.value].flat()[0]; + input?.focus(); + input?.select(); }); } @@ -294,6 +299,7 @@ export default defineComponent({ startTimestamp, topConfidence, trackAttributeColumns, + trackFilters, typeInputRef, }; }, @@ -328,10 +334,27 @@ export default defineComponent({ :style="itemStyle" @click="handleClicked" > -
+
+
+
+ +
+
{{ track.trackId }}
@@ -516,15 +539,29 @@ export default defineComponent({ background-color: #2a2a2a; } + .track-lead-compact { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 24px; + margin-right: 6px; + } + .type-color-box-compact { min-width: 10px; max-width: 10px; min-height: 10px; max-height: 10px; - margin-right: 6px; border-radius: 2px; } + .track-checkbox-compact { + ::v-deep .v-input--selection-controls__input { + margin-right: 0; + } + } + .trackNumber-compact { font-size: 14px; font-weight: bold; @@ -605,6 +642,9 @@ export default defineComponent({ } .track-length { + width: 64px; + min-width: 64px; + max-width: 64px; text-align: center; } From 004850b3d998e3edafa95b08332f9d142dc4af98 Mon Sep 17 00:00:00 2001 From: Matt Dawkins Date: Mon, 28 Sep 2026 23:55:29 -0400 Subject: [PATCH 2/3] Line the bottom track list's headers up with their columns The header row was padded 8px more than the rows, so each label sat to the right of its values. ID and its values are now centered on each other. --- client/src/components/Tracks/TrackList.vue | 11 +++++++++-- .../Tracks/bottombar/BottomBarTrackItemView.vue | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/client/src/components/Tracks/TrackList.vue b/client/src/components/Tracks/TrackList.vue index 3c5c1c8f6..b19fd721d 100644 --- a/client/src/components/Tracks/TrackList.vue +++ b/client/src/components/Tracks/TrackList.vue @@ -575,6 +575,9 @@ export default defineComponent({ font-weight: 600; } .compact-column-headers { + /* The header is padded 8px more than the rows; without this every label sits right of its column */ + margin-left: -8px; + .col-header { font-size: 10px; color: #888; @@ -604,8 +607,11 @@ export default defineComponent({ min-width: 30px; } .col-id { - /* Matches trackNumber-compact: 30px + 8px margin */ - min-width: 38px; + /* Matches trackNumber-compact: 30px + 8px margin, centered like its values */ + width: 30px; + min-width: 30px; + flex-shrink: 0; + margin-right: 8px; } .col-type { /* Matches track-type-compact: 80px */ @@ -649,6 +655,7 @@ export default defineComponent({ min-width: 64px; max-width: 64px; } + .col-id, .col-conf, .col-length { position: relative; diff --git a/client/src/components/Tracks/bottombar/BottomBarTrackItemView.vue b/client/src/components/Tracks/bottombar/BottomBarTrackItemView.vue index a5cbe30f6..932504d3e 100644 --- a/client/src/components/Tracks/bottombar/BottomBarTrackItemView.vue +++ b/client/src/components/Tracks/bottombar/BottomBarTrackItemView.vue @@ -567,6 +567,7 @@ export default defineComponent({ font-weight: bold; margin-right: 8px; min-width: 30px; + text-align: center; } .track-frame-start, From 7c70cb6088db8bb217a30aea59815548390c0787 Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Tue, 29 Sep 2026 10:16:42 -0400 Subject: [PATCH 3/3] minor fixes --- .../bottombar/BottomBarTrackItemView.vue | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/client/src/components/Tracks/bottombar/BottomBarTrackItemView.vue b/client/src/components/Tracks/bottombar/BottomBarTrackItemView.vue index 932504d3e..318b1d4b1 100644 --- a/client/src/components/Tracks/bottombar/BottomBarTrackItemView.vue +++ b/client/src/components/Tracks/bottombar/BottomBarTrackItemView.vue @@ -2,6 +2,7 @@ import { computed, defineComponent, nextTick, PropType, ref, watch, } from 'vue'; +import type { ComponentPublicInstance } from 'vue'; import { ColumnVisibilitySettings } from 'dive-common/store/settings'; import TooltipBtn from '../../TooltipButton.vue'; import { @@ -52,10 +53,13 @@ export default defineComponent({ const notesInputRef = ref(null); const editingAttributeKey = ref(null); const editAttributeValue = ref(''); - // Rendered inside a v-for, where Vue collects the refs into an array - const attributeInputRef = ref(null); + const attributeInputRef = ref(null); const localAttributeDisplay = ref>({}); + function setAttributeInputRef(el: Element | ComponentPublicInstance | null) { + attributeInputRef.value = el instanceof HTMLInputElement ? el : null; + } + watch(() => props.track.id, () => { localNotesDisplay.value = ''; localAttributeDisplay.value = {}; @@ -213,9 +217,8 @@ export default defineComponent({ editingAttributeKey.value = attrKey; nextTick(() => { // Without focus the field would never blur, and so never close - const input = [attributeInputRef.value].flat()[0]; - input?.focus(); - input?.select(); + attributeInputRef.value?.focus(); + attributeInputRef.value?.select(); }); } @@ -263,7 +266,7 @@ export default defineComponent({ return { allTypes, - attributeInputRef, + setAttributeInputRef, cancelEditAttribute, cancelEditConfidence, cancelEditNotes, @@ -448,7 +451,7 @@ export default defineComponent({