diff --git a/client/src/components/Tracks/TrackList.vue b/client/src/components/Tracks/TrackList.vue index 4290ae0cc..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; @@ -600,12 +603,15 @@ 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 */ - 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 */ @@ -643,6 +649,13 @@ 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-id, .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..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 { @@ -26,6 +27,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 }, @@ -53,6 +56,10 @@ export default defineComponent({ 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 = {}; @@ -209,6 +216,7 @@ export default defineComponent({ editAttributeValue.value = getAttributeValue(attrKey); editingAttributeKey.value = attrKey; nextTick(() => { + // Without focus the field would never blur, and so never close attributeInputRef.value?.focus(); attributeInputRef.value?.select(); }); @@ -258,7 +266,7 @@ export default defineComponent({ return { allTypes, - attributeInputRef, + setAttributeInputRef, cancelEditAttribute, cancelEditConfidence, cancelEditNotes, @@ -294,6 +302,7 @@ export default defineComponent({ startTimestamp, topConfidence, trackAttributeColumns, + trackFilters, typeInputRef, }; }, @@ -328,10 +337,27 @@ export default defineComponent({ :style="itemStyle" @click="handleClicked" > -
+
+
+
+ +
+
{{ track.trackId }}
@@ -425,7 +451,7 @@ export default defineComponent({