feat: create a new canvas by copying an existing one
Add a 'Copy from existing' option to the New Canvas modal. It lists every
canvas with node/group/text counts; picking one deep-copies its nodes, edges,
parent/child links and canvas state (viewport, custom style, floor plan) into
a fresh design.
Backend: POST /designs/{source_id}/copy remaps node ids, re-points edges and
parent links, and clones canvas state; GET /designs now returns per-design
counts for the picker. Standalone mode clones the localStorage canvas.
Closes #216
ha-relevant: maybe
This commit is contained in:
@@ -65,6 +65,60 @@ describe('DesignModal', () => {
|
||||
expect(onSubmit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
describe('copy from existing', () => {
|
||||
const sourceDesigns = [
|
||||
{ id: 's1', name: 'Home Net', icon: 'network', design_type: 'network' as const,
|
||||
created_at: '', updated_at: '', node_count: 4, group_count: 1, text_count: 2 },
|
||||
{ id: 's2', name: 'Lab', icon: 'server', design_type: 'network' as const,
|
||||
created_at: '', updated_at: '', node_count: 7, group_count: 0, text_count: 0 },
|
||||
]
|
||||
|
||||
it('offers no copy option when there are no source designs', () => {
|
||||
renderModal({ sourceDesigns: [] })
|
||||
expect(screen.queryByRole('button', { name: 'Copy from existing' })).toBeNull()
|
||||
})
|
||||
|
||||
it('shows the source list with counts once "Copy from existing" is chosen', () => {
|
||||
renderModal({ sourceDesigns })
|
||||
// Hidden until the user opts into copying.
|
||||
expect(screen.queryByText('Home Net')).toBeNull()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Copy from existing' }))
|
||||
expect(screen.getByText('Home Net')).toBeDefined()
|
||||
expect(screen.getByText('4 nodes · 1 groups · 2 text')).toBeDefined()
|
||||
expect(screen.getByText('7 nodes · 0 groups · 0 text')).toBeDefined()
|
||||
})
|
||||
|
||||
it('includes sourceId (first design by default) on submit', () => {
|
||||
const { onSubmit } = renderModal({ sourceDesigns })
|
||||
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Copy of Home' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Copy from existing' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Create' }))
|
||||
expect(onSubmit).toHaveBeenCalledWith({ name: 'Copy of Home', icon: DEFAULT_DESIGN_ICON, sourceId: 's1' })
|
||||
})
|
||||
|
||||
it('includes the picked sourceId on submit', () => {
|
||||
const { onSubmit } = renderModal({ sourceDesigns })
|
||||
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Copy of Lab' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Copy from existing' }))
|
||||
fireEvent.click(screen.getByRole('radio', { name: /Lab/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Create' }))
|
||||
expect(onSubmit).toHaveBeenCalledWith({ name: 'Copy of Lab', icon: DEFAULT_DESIGN_ICON, sourceId: 's2' })
|
||||
})
|
||||
|
||||
it('omits sourceId when the blank option is kept', () => {
|
||||
const { onSubmit } = renderModal({ sourceDesigns })
|
||||
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Fresh' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Create' }))
|
||||
expect(onSubmit).toHaveBeenCalledWith({ name: 'Fresh', icon: DEFAULT_DESIGN_ICON })
|
||||
expect('sourceId' in onSubmit.mock.calls[0][0]).toBe(false)
|
||||
})
|
||||
|
||||
it('is hidden in edit mode (floor-plan shown)', () => {
|
||||
renderModal({ sourceDesigns, showFloorMap: true, initial: { name: 'Home', icon: DEFAULT_DESIGN_ICON } })
|
||||
expect(screen.queryByRole('button', { name: 'Copy from existing' })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('floor plan section', () => {
|
||||
const fm = {
|
||||
imageData: 'data:image/png;base64,abc',
|
||||
|
||||
Reference in New Issue
Block a user