import * as React from 'react'
import type { StoryFn, Meta } from '@storybook/react-webpack5'
import { ListGrid } from './index'
import type { ListGridProps } from './types'
import type {
GridActionsMenuFullProps,
GridConfirmPayload,
GridRowDropInfo,
GridRowMeta,
} from '../../types'
import { GridCellButton, GridCellDefault } from '../../components'
import { Cancel, Edit } from '@planview/pv-icons'
import { useGridRow } from '../../context/grid-context'
import { GridEditorInput, GridEditorSwitch } from '../../editors'
import { ListItem, ListItemDivider, Menu } from '@planview/pv-uikit'
import GridMeta from '../grid/index.stories'
export default {
title: 'pv-grid/Components/ListGrid',
component: ListGrid,
parameters: {
badges: ['intl'],
},
argTypes: {
rows: GridMeta.argTypes.rows,
columns: GridMeta.argTypes.columns,
loading: GridMeta.argTypes.loading,
emptyContent: GridMeta.argTypes.emptyContent,
expandedRows: GridMeta.argTypes.expandedRows,
onExpandedRowsChange: GridMeta.argTypes.onExpandedRowsChange,
rowDrag: GridMeta.argTypes.rowDrag,
onCellChange: GridMeta.argTypes.onCellChange,
ref: GridMeta.argTypes.ref,
actionsMenu: {
control: { type: 'radio' },
options: ['no actions menu', 'actions menu', 'actions menu async'],
mapping: {
'no actions menu': undefined,
'actions menu': () => (
<>
>
),
'actions menu async': {
Menu({ row, menuProps }: GridActionsMenuFullProps) {
const [loading, setLoading] = React.useState(true)
React.useEffect(() => {
const t = setTimeout(() => setLoading(false), 500)
return () => clearTimeout(t)
}, [])
return (
)
},
},
},
table: {
category: 'Context Menu',
},
},
},
} satisfies Meta
type KpiCategory = {
id: string
label: string
isIntegrated?: boolean
inUse?: boolean
}
export const Default: StoryFn> = ({
columns: _columns,
rows: _rows,
rowDrag: _rowDrag,
...rest
}) => {
const [sortedRows, setSortedRows] = React.useState([
{ id: '1', label: 'Financial' },
{ id: '2', label: 'Customer', inUse: true },
{ id: '3', label: 'Internal Process' },
{ id: '4', label: 'Learning and Growth' },
{ id: '5', label: 'System', isIntegrated: true },
])
return (
resultIds.map(
(id) => rows.find((row) => row.id === id)!
)
)
}
},
}}
{...rest}
/>
)
}
export const ActionsMenu: StoryFn> = ({
columns: _columns,
rows: _rows,
...rest
}) => {
const [sortedRows, setSortedRows] = React.useState([
{ id: '1', label: 'Financial' },
{ id: '2', label: 'Customer', inUse: true },
{ id: '3', label: 'Internal Process' },
{ id: '4', label: 'Learning and Growth' },
{ id: '5', label: 'System', isIntegrated: true },
])
return (
resultIds.map(
(id) => rows.find((row) => row.id === id)!
)
)
}
},
}}
actionsMenu={() => (
<>
>
)}
/>
)
}
type KpiTreeCategory = {
id: string
label: string
isIntegrated?: boolean
inUse?: boolean
parentId: string | null
index: number
}
function sortByIndex(a: KpiTreeCategory, b: KpiTreeCategory) {
return a.index - b.index
}
export const Tree: StoryFn> = () => {
const [sourceRows, setSourceRows] = React.useState([
{ id: '1', label: 'Financial', parentId: null, index: 0 },
{ id: '2', label: 'Customer', inUse: true, parentId: '1', index: 0 },
{ id: '3', label: 'Internal Process', parentId: null, index: 1 },
{ id: '4', label: 'Learning and Growth', parentId: '1', index: 1 },
{
id: '5',
label: 'System',
isIntegrated: true,
parentId: null,
index: 2,
},
{
id: '6',
label: 'System Subsection',
isIntegrated: true,
parentId: '5',
index: 0,
},
])
const rows = React.useMemo(() => {
// These three items are needed by the grid
const ids: string[] = []
const data = new Map()
const meta = new Map>()
const sortedRows = sourceRows.concat()
sortedRows.sort(sortByIndex)
sortedRows.forEach((row) => {
// Populate the raw data map
data.set(row.id, row)
if (row.parentId === null) {
// If there is no parentId, this row is at the root level
// so add it to the root `ids` array.
ids.push(row.id)
} else {
// This is a child, so fetch its parent's meta data if it exists
// and add it to the `children` array
const children = meta.get(row.parentId)?.children ?? []
children.push(row.id)
meta.set(row.parentId, { type: 'tree', children })
}
})
return { ids, data, meta }
}, [sourceRows])
const onDrop = React.useCallback(
(info: GridRowDropInfo) => {
const draggedIds = new Set(info.draggedRowIds)
const impactedParentIds = new Set(
info.draggedRows.map((d) => d.row.parentId)
)
// This is where you could kick off an API request to move the
// impacted items on the server. The code below would be needed
// regardless to provide an optimistic change to the user
// immediately. In the event of a server error, you could revert
// the activities to their original state
setSourceRows((prev) => {
// Make deep copy of activities, to avoid mutating in-memory
const newRows = structuredClone(prev)
// Find rows in their original order
// You can't go off of raw index order since absolute position
// in the list is influenced by parent nesting.
const draggedRows = info.draggedRowIds.map(
(id) => newRows.find((d) => d.id === id)!
)
// Steps 1 & 2 – Remove from parents and heal siblings.
// parentId may be `null` indicating root
impactedParentIds.forEach((parentId) => {
const oldSiblings = newRows.filter(
(r) => r.parentId === parentId && !draggedIds.has(r.id)
)
oldSiblings.sort(sortByIndex)
oldSiblings.forEach((sibling, index) => {
sibling.index = index
})
})
// Step 3 - Insert dragged items in the correct location
// and adjust sibling indexes
const newSiblings = newRows.filter(
(r) =>
r.parentId === info.targetParentId &&
!draggedIds.has(r.id)
)
newSiblings.sort(sortByIndex)
newSiblings.splice(
// If rows were dropped on a parent and not in a specific
// location, the targetIndex will be null. In this case
// they are inserted at the end
info.targetIndex == null
? newSiblings.length
: info.targetIndex,
0,
...draggedRows
)
// Step 4 - Adjust indexes of siblings and dropped rows
// This also sets the parentId for the dropped rows as the other
// siblings already have this value set
newSiblings.forEach((sibling, index) => {
sibling.index = index
sibling.parentId = info.targetParentId
})
return newRows
})
},
[]
)
return (
}
tooltip="Delete category"
onClick={() => {
// void deleteRow(rowId)
}}
/>
)
}
return (
)
},
},
},
{
id: 'actions',
label: 'Actions',
width: 36,
cell: {
Renderer({ tabIndex, rowId }) {
return (
}
tooltip="Delete category"
onClick={() => {
// void deleteRow(rowId)
}}
/>
)
},
},
},
]}
rows={rows}
rowDrag={{
enableLeafConversion: true,
onDrop,
}}
/>
)
}
export const ReorderableAndEditable: StoryFn<
ListGridProps
> = () => {
const [sortedRows, setSortedRows] = React.useState([
{ id: '1', label: 'Financial' },
{ id: '2', label: 'Customer', inUse: true },
{ id: '3', label: 'Internal Process' },
{ id: '4', label: 'Learning and Growth' },
{ id: '5', label: 'System', isIntegrated: true },
])
return (
(rowId)
if (row.inUse || row.isIntegrated) {
return (
)
}
return (
}
tooltip="Delete category"
onClick={() => {
// void deleteRow(rowId)
}}
/>
)
},
},
},
]}
rows={sortedRows}
rowDrag={{
previewColumnId: 'label',
onDrop({ resultIds }) {
if (resultIds) {
setSortedRows((rows) =>
resultIds.map(
(id) => rows.find((row) => row.id === id)!
)
)
}
},
}}
onCellChange={(payload: GridConfirmPayload) => {
setSortedRows((rows) =>
rows.map((row) => {
if (row.id === payload.rowId) {
return {
...row,
[payload.columnId]: payload.nextValue,
}
}
return row
})
)
}}
/>
)
}
export const ActiveAndEditable: StoryFn> = () => {
const [sortedRows, setSortedRows] = React.useState([
{ id: '1', label: 'Row 1', enabled: false },
{ id: '2', label: 'Row 2', enabled: true },
{ id: '3', label: 'Row 3', enabled: false },
])
return (
}
/>
)
},
},
},
]}
rows={sortedRows}
rowDrag={{
previewColumnId: 'label',
onDrop({ resultIds }) {
if (resultIds) {
setSortedRows((rows) =>
resultIds.map(
(id) => rows.find((row) => row.id === id)!
)
)
}
},
}}
onCellChange={(payload: GridConfirmPayload) => {
setSortedRows((rows) =>
rows.map((row) => {
if (row.id === payload.rowId) {
return {
...row,
[payload.columnId]: payload.nextValue,
}
}
return row
})
)
}}
/>
)
}