Skip to content

Commit

Permalink
Merge pull request #90 from commitd/sh/layout
Browse files Browse the repository at this point in the history
feat: add a flag to layout on change
  • Loading branch information
stuarthendren authored Mar 12, 2023
2 parents ead751b + aeac4f2 commit 229bc64
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 12 deletions.
2 changes: 1 addition & 1 deletion apps/docs/src/Graph.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Box, Column } from '@committed/components'
import { cytoscapeRenderer, Graph, GraphDebugControl } from '@committed/components-graph'
import { Meta } from '@storybook/react'
import React, { useState } from 'react'
import { Graph, cytoscapeRenderer, GraphDebugControl,} from '@committed/components-graph'
import { exampleModel } from './StoryUtil'

export default {
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/GraphToolbar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function isFunction(
const empty = {
zoom: false,
layout: false,
relayout: false,
refit: false,
hide: false,
size: false,
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions packages/graph/src/graph/LayoutModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ it('Validating an validated layout does nothing', () => {
const model = layoutModel.setLayout('grid').validate()
expect(model.validate()).toBe(model)
})

it('Changing the onChange does not invalidate the layout', () => {
expect(layoutModel.setOnChange(false).isDirty()).toBe(false)
expect(layoutModel.setOnChange(true).isDirty()).toBe(false)
})
20 changes: 15 additions & 5 deletions packages/graph/src/graph/LayoutModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { CustomGraphLayout, GraphLayout, PresetGraphLayout } from './types'

export class LayoutModel {
private readonly layout: GraphLayout
private readonly onChange: boolean
private readonly invalidated: boolean

static createDefault(): LayoutModel {
return new LayoutModel('force-directed', true)
return new LayoutModel('force-directed', true, true)
}

constructor(layout: GraphLayout, invalidated = true) {
constructor(layout: GraphLayout, onChange = true, invalidated = true) {
this.layout = layout
this.onChange = onChange
this.invalidated = invalidated
}

Expand All @@ -21,24 +23,32 @@ export class LayoutModel {
return this.invalidated
}

isOnChange(): boolean {
return this.onChange
}

validate(): LayoutModel {
if (!this.invalidated) {
return this
} else {
return new LayoutModel(this.layout, false)
return new LayoutModel(this.layout, this.onChange, false)
}
}

invalidate(): LayoutModel {
if (this.invalidated) {
return this
} else {
return new LayoutModel(this.layout, true)
return new LayoutModel(this.layout, this.onChange, true)
}
}

setLayout(layout: GraphLayout): LayoutModel {
return new LayoutModel(layout, true)
return new LayoutModel(layout, this.onChange, true)
}

setOnChange(onChange: boolean): LayoutModel {
return new LayoutModel(this.layout, onChange, false)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { Box, Button, Flex, Select, SelectItem } from '@committed/components'
import {
Box,
Button,
Checkbox,
Flex,
Select,
SelectItem,
} from '@committed/components'
import { Generator, GraphModel, PresetGraphLayout } from '@committed/graph'
import { defaultLayouts } from '../../graph'
import React from 'react'
import { defaultLayouts } from '../../graph'

const { addRandomNode, addRandomEdge, removeRandomNode, removeRandomEdge } =
Generator
Expand Down Expand Up @@ -44,6 +51,18 @@ export const GraphDebugControl: React.FC<GraphDebugControlProps> = ({
<Button onClick={() => onChange(removeRandomEdge(model))}>
Remove Random Edge
</Button>
<Checkbox
label="Layout on change"
checked={model.getCurrentLayout().isOnChange()}
onCheckedChange={(checked: boolean) =>
onChange(
GraphModel.applyLayout(
model,
model.getCurrentLayout().setOnChange(checked)
)
)
}
/>
<Button
onClick={() =>
onChange(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Graph } from '../Graph'
const empty = {
zoom: false,
layout: false,
relayout: false,
refit: false,
hide: false,
size: false,
Expand Down Expand Up @@ -108,6 +109,10 @@ export const Layout: React.FC<TemplateProps> = (props: TemplateProps) => (
<Template {...empty} layout {...props} />
)

export const Relayout: React.FC<TemplateProps> = (props: TemplateProps) => (
<Template {...empty} relayout {...props} />
)

export const SizeBy: React.FC<TemplateProps> = (props: TemplateProps) => (
<Template {...empty} size {...props} />
)
Expand Down
13 changes: 12 additions & 1 deletion packages/react/src/components/GraphToolbar/GraphToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { GraphLayoutOptions } from './GraphLayoutOptions'
import { GraphLayoutRun } from './GraphLayoutRun'
import { Hide } from './Hide'
import { Refit } from './Refit'
import { ReLayout } from './ReLayout'
import { SizeBy } from './SizeBy'
import { Zoom } from './Zoom'

Expand Down Expand Up @@ -76,6 +77,7 @@ export type GraphToolbarProps = CSSProps &
layouts?: GraphLayout[]
zoom?: boolean
layout?: boolean
relayout?: boolean
refit?: boolean
hide?: boolean
size?: boolean
Expand All @@ -96,11 +98,13 @@ export const GraphToolbar: React.FC<GraphToolbarProps> = ({
layouts = [],
zoom = true,
layout = true,
relayout = true,
refit = true,
hide = true,
size = true,
buttonVariant = 'tertiary',
css,
children,
...props
}) => {
const menuItems = useMemo(() => {
Expand All @@ -118,6 +122,12 @@ export const GraphToolbar: React.FC<GraphToolbarProps> = ({
)
}

if (relayout) {
items.push(
<ReLayout key="relayout" model={model} onModelChange={onModelChange} />
)
}

if (layout && layouts.length > 0) {
items.push(
<GraphLayoutOptions
Expand All @@ -129,7 +139,7 @@ export const GraphToolbar: React.FC<GraphToolbarProps> = ({
)
}
return items.filter((item) => item !== null)
}, [hide, layout, layouts, model, onModelChange, size])
}, [hide, layout, layouts, model, onModelChange, size, relayout])

return (
<StyledToolbar css={css as any} {...props}>
Expand Down Expand Up @@ -157,6 +167,7 @@ export const GraphToolbar: React.FC<GraphToolbarProps> = ({
onModelChange={onModelChange}
/>
)}
{children}
{menuItems.length > 0 && (
<Menu>
<MenuTrigger>
Expand Down
24 changes: 24 additions & 0 deletions packages/react/src/components/GraphToolbar/ReLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import { renderLight, screen, userEvent } from '../../test/setup'
import { Relayout } from './GraphToolbar.test'

it('Can select to size by', () => {
renderLight(<Relayout withGraph={false} />)
userEvent.tab()
userEvent.keyboard('{enter}')
expect(
screen
.getByRole('menuitemcheckbox', { name: /Layout on change/i })
.getAttribute('aria-checked')
).toBe('true')
userEvent.click(
screen.getByRole('menuitemcheckbox', { name: /Layout on change/i })
)
userEvent.tab()
userEvent.keyboard('{enter}')
expect(
screen
.getByRole('menuitemcheckbox', { name: /Layout on change/i })
.getAttribute('aria-checked')
).toBe('false')
})
50 changes: 50 additions & 0 deletions packages/react/src/components/GraphToolbar/ReLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { CSSProps, MenuCheckboxItem, VariantProps } from '@committed/components'
import { GraphModel } from '@committed/graph'
import React, { useCallback } from 'react'

export type ReLayoutProps = CSSProps &
VariantProps<typeof MenuCheckboxItem> & {
/** Declarative definition of graph state */
model: GraphModel
/** The graph model change callback */
onModelChange: (
model: GraphModel | ((model2: GraphModel) => GraphModel)
) => void
}

/**
* A GraphToolbar sub-component to relayout on graph change
*/
export const ReLayout: React.FC<ReLayoutProps> = ({
model,
onModelChange,
css,
...props
}) => {
const handleToggle = useCallback((): void => {
onModelChange(
GraphModel.applyLayout(
model,
model
.getCurrentLayout()
.setOnChange(!model.getCurrentLayout().isOnChange())
)
)
}, [model, onModelChange])

return (
<>
<MenuCheckboxItem
css={css as any}
{...props}
key="hideEdgeLabels"
checked={model.getCurrentLayout().isOnChange()}
onCheckedChange={handleToggle}
>
Layout on change
</MenuCheckboxItem>
</>
)
}
4 changes: 2 additions & 2 deletions packages/react/src/graph/renderer/CytoscapeRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ it('can be rendered', () => {
/>
)
expect(asFragment()).toBeDefined()
expect(cytoscape.addListener).toHaveBeenCalledTimes(8)
expect(cytoscape.addListener).toHaveBeenCalledTimes(10)

unmount()

expect(cytoscape.removeListener).toHaveBeenCalledTimes(8)
expect(cytoscape.removeListener).toHaveBeenCalledTimes(10)
})

it('layout command triggers layout', () => {
Expand Down
17 changes: 17 additions & 0 deletions packages/react/src/graph/renderer/CytoscapeRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,21 @@ const Renderer: GraphRenderer<CyGraphRendererOptions>['render'] = ({
},
[updateSelection]
)
const updateLayout = useCallback(() => {
if (graphModel.getCurrentLayout().isOnChange()) {
triggerLayout(layout)
}
}, [graphModel, triggerLayout, layout])
const disableLayoutOnChange = useCallback(() => {
if (graphModel.getCurrentLayout().isOnChange()) {
onChange((model) =>
GraphModel.applyLayout(
model,
model.getCurrentLayout().setOnChange(false)
)
)
}
}, [graphModel, onChange])
const layoutStarting = useCallback(() => {
console.debug('Layout started')
layoutStart.current = Date.now()
Expand All @@ -286,6 +301,8 @@ const Renderer: GraphRenderer<CyGraphRendererOptions>['render'] = ({
useCyListener(cytoscape, unselectNode, 'unselect', 'node')
useCyListener(cytoscape, selectEdge, 'select', 'edge')
useCyListener(cytoscape, unselectEdge, 'unselect', 'edge')
useCyListener(cytoscape, updateLayout, 'add remove', '*')
useCyListener(cytoscape, disableLayoutOnChange, 'drag', 'node')
useCyListener(cytoscape, layoutStarting, 'layoutstart')
useCyListener(cytoscape, layoutStopping, 'layoutstop')
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down

0 comments on commit 229bc64

Please sign in to comment.