0.13.1: new track-group lands above the active layer
addTrackGroup takes an optional anchorTrackId and inserts directly above it in trackOrder; anchors nested inside another group resolve to their parent's slot. The timeline's group-add button forwards selectedLayerId so the new group lands where the user is working instead of always jumping to the top. Covered by store + UI tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
86ec7aa835
commit
8aa91fc27b
5 files changed
+113
-5
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ruwio",
|
"name": "ruwio",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.13.0",
|
"version": "0.13.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1592,7 +1592,10 @@ function toggleGroupCollapsed(groupId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addGroup() {
|
function addGroup() {
|
||||||
store.addTrackGroup()
|
// Drop the group directly above whatever the user is currently
|
||||||
|
// working on — layer selection is the only active-track signal
|
||||||
|
// today. Falls back to the top of the stack when nothing's active.
|
||||||
|
store.addTrackGroup(undefined, store.selectedLayerId ?? null)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renameGroupPrompt(groupId: string, current: string) {
|
async function renameGroupPrompt(groupId: string, current: string) {
|
||||||
|
|||||||
@@ -2666,6 +2666,41 @@ export function registerAllTests(): void {
|
|||||||
expect(s.scene.trackOrder![0]).toBe(g.id)
|
expect(s.scene.trackOrder![0]).toBe(g.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('addTrackGroup with an anchor inserts directly above that track', () => {
|
||||||
|
const s = makeFreshStore()
|
||||||
|
const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#111' })
|
||||||
|
const l1 = s.addLayer(a.id)! // trackOrder ends up [l2, l1] via unshift
|
||||||
|
const l2 = s.addLayer(a.id)!
|
||||||
|
// Anchor on the second layer → group lands at its index, shoving it down.
|
||||||
|
const g = s.addTrackGroup('G', l1.id)
|
||||||
|
const order = s.scene.trackOrder ?? []
|
||||||
|
const gIdx = order.indexOf(g.id)
|
||||||
|
const l1Idx = order.indexOf(l1.id)
|
||||||
|
expect(gIdx).toBe(l1Idx - 1)
|
||||||
|
void l2
|
||||||
|
})
|
||||||
|
|
||||||
|
test('addTrackGroup with an anchor inside an existing group lands above the parent group', () => {
|
||||||
|
const s = makeFreshStore()
|
||||||
|
const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#111' })
|
||||||
|
const l = s.addLayer(a.id)!
|
||||||
|
const parent = s.addTrackGroup('Parent')
|
||||||
|
s.moveTrackIntoGroup(l.id, parent.id)
|
||||||
|
// Anchor is nested inside `parent` — new group must sit above `parent`,
|
||||||
|
// not attempt to nest itself inside another group.
|
||||||
|
const fresh = s.addTrackGroup('Fresh', l.id)
|
||||||
|
const order = s.scene.trackOrder ?? []
|
||||||
|
expect(order.indexOf(fresh.id)).toBe(order.indexOf(parent.id) - 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('addTrackGroup with an unknown anchor falls back to the top', () => {
|
||||||
|
const s = makeFreshStore()
|
||||||
|
const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#111' })
|
||||||
|
s.addLayer(a.id)!
|
||||||
|
const g = s.addTrackGroup('G', 'ghost-id')
|
||||||
|
expect(s.scene.trackOrder![0]).toBe(g.id)
|
||||||
|
})
|
||||||
|
|
||||||
test('moveTrackIntoGroup moves track from top-level to group childIds', () => {
|
test('moveTrackIntoGroup moves track from top-level to group childIds', () => {
|
||||||
const s = makeFreshStore()
|
const s = makeFreshStore()
|
||||||
const a = s.addAsset({ kind: 'rect', name: 'G2', width: 10, height: 10, fill: '#222' })
|
const a = s.addAsset({ kind: 'rect', name: 'G2', width: 10, height: 10, fill: '#222' })
|
||||||
@@ -5027,6 +5062,51 @@ export function registerAllTests(): void {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
suite('ui — Timeline new-group button', () => {
|
||||||
|
test('clicking the new-group button with a selected layer inserts the group above it', async () => {
|
||||||
|
const s = makeFreshStore()
|
||||||
|
const a = s.addAsset({ kind: 'rect', name: 'Anchor', width: 10, height: 10, fill: '#111' })
|
||||||
|
const top = s.addLayer(a.id)!
|
||||||
|
const anchor = s.addLayer(a.id)!
|
||||||
|
s.selectLayer(anchor.id)
|
||||||
|
const handle = mountComponent(Timeline, {})
|
||||||
|
await nextTick()
|
||||||
|
const btn = handle.host.querySelector(
|
||||||
|
`button[aria-label="${i18nT('timeline.addGroupTooltip')}"]`,
|
||||||
|
) as HTMLButtonElement
|
||||||
|
expect(btn).toBeTruthy()
|
||||||
|
btn.click()
|
||||||
|
await nextTick()
|
||||||
|
const order = s.scene.trackOrder ?? []
|
||||||
|
const groupId = (s.scene.groups ?? [])[0]?.id
|
||||||
|
expect(groupId).toBeTruthy()
|
||||||
|
const gIdx = order.indexOf(groupId!)
|
||||||
|
const anchorIdx = order.indexOf(anchor.id)
|
||||||
|
// Group sits right above the anchor layer, pushing it down by one slot.
|
||||||
|
expect(gIdx).toBe(anchorIdx - 1)
|
||||||
|
// Unrelated `top` layer keeps its own slot — not dragged along.
|
||||||
|
expect(order.indexOf(top.id) >= 0).toBe(true)
|
||||||
|
handle.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('clicking the new-group button with no selection unshifts to the top (regression)', async () => {
|
||||||
|
const s = makeFreshStore()
|
||||||
|
const a = s.addAsset({ kind: 'rect', name: 'X', width: 10, height: 10, fill: '#111' })
|
||||||
|
s.addLayer(a.id)
|
||||||
|
s.selectLayer(null)
|
||||||
|
const handle = mountComponent(Timeline, {})
|
||||||
|
await nextTick()
|
||||||
|
const btn = handle.host.querySelector(
|
||||||
|
`button[aria-label="${i18nT('timeline.addGroupTooltip')}"]`,
|
||||||
|
) as HTMLButtonElement
|
||||||
|
btn.click()
|
||||||
|
await nextTick()
|
||||||
|
const groupId = (s.scene.groups ?? [])[0]?.id
|
||||||
|
expect(s.scene.trackOrder?.[0]).toBe(groupId)
|
||||||
|
handle.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
suite('ui — Timeline audio track', () => {
|
suite('ui — Timeline audio track', () => {
|
||||||
test('audio track renders as an .audio-track lane with one block per clip', async () => {
|
test('audio track renders as an .audio-track lane with one block per clip', async () => {
|
||||||
const s = makeFreshStore()
|
const s = makeFreshStore()
|
||||||
|
|||||||
+27
-2
@@ -2660,14 +2660,39 @@ export const useSceneStore = defineStore('scene', {
|
|||||||
else inst.rotation = r
|
else inst.rotation = r
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addTrackGroup(name?: string): TrackGroup {
|
/** Create an empty group. When `anchorTrackId` is given the group
|
||||||
|
* lands directly above that track in trackOrder — resolving to
|
||||||
|
* the parent group's position if the anchor sits inside one. Falls
|
||||||
|
* back to the top of the stack when the anchor is missing or the
|
||||||
|
* scene has no trackOrder yet. */
|
||||||
|
addTrackGroup(name?: string, anchorTrackId?: string | null): TrackGroup {
|
||||||
const group: TrackGroup = {
|
const group: TrackGroup = {
|
||||||
id: uid(),
|
id: uid(),
|
||||||
name: name?.trim() || 'Group',
|
name: name?.trim() || 'Group',
|
||||||
childIds: [],
|
childIds: [],
|
||||||
}
|
}
|
||||||
;(this.scene.groups ||= []).push(group)
|
;(this.scene.groups ||= []).push(group)
|
||||||
;(this.scene.trackOrder ||= []).unshift(group.id)
|
const order = (this.scene.trackOrder ||= [])
|
||||||
|
|
||||||
|
let insertAt = 0
|
||||||
|
if (anchorTrackId) {
|
||||||
|
const direct = order.indexOf(anchorTrackId)
|
||||||
|
if (direct >= 0) {
|
||||||
|
insertAt = direct
|
||||||
|
} else {
|
||||||
|
// Anchor is inside a group — put the new group above the
|
||||||
|
// group container instead of trying to wedge in between
|
||||||
|
// group children (nested groups are not supported).
|
||||||
|
const parent = this.scene.groups?.find((g) =>
|
||||||
|
g.childIds.includes(anchorTrackId),
|
||||||
|
)
|
||||||
|
if (parent) {
|
||||||
|
const pi = order.indexOf(parent.id)
|
||||||
|
if (pi >= 0) insertAt = pi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
order.splice(insertAt, 0, group.id)
|
||||||
return group
|
return group
|
||||||
},
|
},
|
||||||
/** Create a new group and move every currently-selected layer
|
/** Create a new group and move every currently-selected layer
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
export const APP_VERSION = '0.13.0'
|
export const APP_VERSION = '0.13.1'
|
||||||
export const APP_NAME = 'RUWIO'
|
export const APP_NAME = 'RUWIO'
|
||||||
Reference in new issue
Block a user