Repository: Automattic/wp-calypso
Pull request #103460: Add new story that combines DataViews and DataForm
Author: login: oandregal
Created: 2025-05-15T11:11:58Z | Merged: 2025-05-16T08:44:00Z

## Proposed Changes

This PR adds a new story that combines DataViews and DataForm.

https://github.com/user-attachments/assets/074bbcbf-1cc9-4738-a36f-04c91bbd54a8


## Why are these changes being made?

In several instances lately, I've found myself wanting to compare changes to both. It's also good to have an example for inspiration.

## Testing Instructions

Run the storybook and access the new story:

```sh
yarn workspace @automattic/dataviews storybook:start
```



Diff:
diff --git a/packages/dataviews/CHANGELOG.automattic.md b/packages/dataviews/CHANGELOG.automattic.md
index 4249c71fee50..961ccf1f497a 100644
--- a/packages/dataviews/CHANGELOG.automattic.md
+++ b/packages/dataviews/CHANGELOG.automattic.md
@@ -7,6 +7,7 @@
 - Pin the actions column on the table view when the width is insufficient.
 - Bring changes from @wordpress/dataviews 4.19.0 (no updates in this version).
 - Enhance filter component styles.
+- Adds new story that combines DataViews and DataForm.
 
 ## 0.1.1
 
diff --git a/packages/dataviews/src/components/dataviews-dataform-story/index.story.tsx b/packages/dataviews/src/components/dataviews-dataform-story/index.story.tsx
new file mode 100644
index 000000000000..5c63aa88ae37
--- /dev/null
+++ b/packages/dataviews/src/components/dataviews-dataform-story/index.story.tsx
@@ -0,0 +1,156 @@
+/**
+ * WordPress dependencies
+ */
+import { useState, useMemo } from '@wordpress/element';
+import {
+	__experimentalHStack as HStack,
+	__experimentalVStack as VStack,
+	Card,
+	CardHeader,
+	CardBody,
+} from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import DataViews from '../dataviews/index';
+import DataForm from '../dataform/index';
+import {
+	actions,
+	data,
+	fields,
+	type SpaceObject,
+} from '../dataviews/stories/fixtures';
+import { filterSortAndPaginate } from '../../filter-and-sort-data-view';
+import type { View, Form } from '../../types';
+
+const meta = {
+	title: 'DataViews/DataViewsAndDataForm',
+	component: DataForm,
+	argTypes: {
+		type: {
+			control: { type: 'select' },
+			description:
+				'Chooses the default layout of each field. "regular" is the default layout.',
+			options: [ 'default', 'regular', 'panel' ],
+		},
+		labelPosition: {
+			control: { type: 'select' },
+			description: 'Chooses the label position of the layout.',
+			options: [ 'default', 'top', 'side', 'none' ],
+		},
+	},
+} as const;
+export default meta;
+
+const defaultLayouts = {
+	table: {},
+	grid: {},
+	list: {},
+};
+
+export const Default = ( {
+	type,
+	labelPosition,
+}: {
+	type: 'default' | 'regular' | 'panel';
+	labelPosition: 'default' | 'top' | 'side' | 'none';
+} ) => {
+	const form = useMemo(
+		() => ( {
+			type,
+			labelPosition,
+			fields: [
+				'title',
+				'description',
+				'type',
+				'isPlanet',
+				'satellites',
+				'date',
+			],
+		} ),
+		[ type, labelPosition ]
+	) as Form;
+
+	const [ view, setView ] = useState< View >( {
+		type: 'table' as const,
+		search: '',
+		page: 1,
+		perPage: 10,
+		layout: {},
+		filters: [],
+		titleField: 'title',
+		descriptionField: 'description',
+		mediaField: 'image',
+		fields: [ 'type', 'isPlanet', 'satellites', 'categories', 'date' ],
+	} );
+
+	const [ selectedIds, setSelectedIds ] = useState< number[] >( [] );
+	const [ modifiedData, setModifiedData ] = useState< SpaceObject[] >( data );
+
+	const { data: shownData, paginationInfo } = useMemo( () => {
+		return filterSortAndPaginate( modifiedData, view, fields );
+	}, [ modifiedData, view ] );
+
+	let selectedItem =
+		( selectedIds.length === 1 &&
+			shownData.find( ( item ) => item.id === selectedIds[ 0 ] ) ) ||
+		null;
+
+	return (
+		<HStack alignment="stretch">
+			<div style={ { flex: 2 } }>
+				<DataViews
+					getItemId={ ( item ) => item.id.toString() }
+					data={ shownData }
+					paginationInfo={ paginationInfo }
+					view={ view }
+					fields={ fields }
+					onChangeView={ setView }
+					actions={ actions }
+					defaultLayouts={ defaultLayouts }
+					selection={ selectedIds.map( ( id ) => id.toString() ) }
+					onChangeSelection={ ( newSelection ) =>
+						setSelectedIds(
+							newSelection.map( ( id ) => parseInt( id, 10 ) )
+						)
+					}
+					onClickItem={ ( item ) => alert( 'clicked ' + item.title ) }
+				/>
+			</div>
+			{ selectedItem ? (
+				<VStack alignment="top">
+					<DataForm
+						data={ selectedItem }
+						form={ form }
+						fields={ fields }
+						onChange={ ( updatedValues ) => {
+							const updatedItem = {
+								...selectedItem,
+								...updatedValues,
+							};
+
+							setModifiedData(
+								modifiedData.map( ( item ) =>
+									item.id === selectedItem.id
+										? updatedItem
+										: item
+								)
+							);
+						} }
+					/>
+				</VStack>
+			) : (
+				<VStack alignment="center">
+					<span
+						style={ {
+							color: '#888',
+						} }
+					>
+						Please, select a single item.
+					</span>
+				</VStack>
+			) }
+		</HStack>
+	);
+};
diff --git a/packages/dataviews/src/components/dataviews/stories/fixtures.tsx b/packages/dataviews/src/components/dataviews/stories/fixtures.tsx
index b3fe96cc85ee..1b38632375ea 100644
--- a/packages/dataviews/src/components/dataviews/stories/fixtures.tsx
+++ b/packages/dataviews/src/components/dataviews/stories/fixtures.tsx
@@ -649,6 +649,7 @@ export const fields: Field< SpaceObject >[] = [
 	{
 		label: 'Title',
 		id: 'title',
+		type: 'text',
 		enableHiding: false,
 		enableGlobalSearch: true,
 	},
@@ -689,6 +690,7 @@ export const fields: Field< SpaceObject >[] = [
 	{
 		label: 'Description',
 		id: 'description',
+		type: 'text',
 		enableSorting: false,
 		enableGlobalSearch: true,
 	},


Reviews:
user: login: ntsekouras
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T11:35:41Z
user: login: ntsekouras
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T11:37:29Z
user: login: oandregal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T15:03:39Z
user: login: ntsekouras
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T17:29:50Z
user: login: oandregal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T07:29:59Z

Review comments:
diff_hunk: @@ -0,0 +1,174 @@
+/**
+ * WordPress dependencies
+ */
+import { useState, useMemo } from '@wordpress/element';
+import {
+	__experimentalHStack as HStack,
+	__experimentalVStack as VStack,
+	Card,
+	CardHeader,
+	CardBody,
+} from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import DataViews from '../dataviews/index';
+import DataForm from '../dataform/index';
+import {
+	actions,
+	data,
+	fields,
+	type SpaceObject,
+} from '../dataviews/stories/fixtures';
+import { filterSortAndPaginate } from '../../filter-and-sort-data-view';
+import type { View, Form } from '../../types';
+
+const meta = {
+	title: 'DataViews/DataViewsAndDataForm',
+	component: DataForm,
path: packages/dataviews/src/components/dataviews-dataform-story/index.story.tsx
user: login: ntsekouras
body: I don't know what's the best way to implement a story that uses two components, but here it's like is part of DataForm and shows those controls etc..

Do we want controls for such an example? If they are irrelevant we could hide them probably.

Related to that, should we add this story as part of the DataViews stories maybe?
created_at: 2025-05-15T11:35:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,174 @@
+/**
+ * WordPress dependencies
+ */
+import { useState, useMemo } from '@wordpress/element';
+import {
+	__experimentalHStack as HStack,
+	__experimentalVStack as VStack,
+	Card,
+	CardHeader,
+	CardBody,
+} from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import DataViews from '../dataviews/index';
+import DataForm from '../dataform/index';
+import {
+	actions,
+	data,
+	fields,
+	type SpaceObject,
+} from '../dataviews/stories/fixtures';
+import { filterSortAndPaginate } from '../../filter-and-sort-data-view';
+import type { View, Form } from '../../types';
+
+const meta = {
+	title: 'DataViews/DataViewsAndDataForm',
+	component: DataForm,
+	argTypes: {
+		type: {
+			control: { type: 'select' },
+			description:
+				'Chooses the default layout of each field. "regular" is the default layout.',
+			options: [ 'default', 'regular', 'panel' ],
+		},
+		labelPosition: {
+			control: { type: 'select' },
+			description: 'Chooses the label position of the layout.',
+			options: [ 'default', 'top', 'side', 'none' ],
+		},
+	},
+} as const;
+export default meta;
+
+const defaultLayouts = {
+	table: {},
+	grid: {},
+	list: {},
+};
+
+export const Default = ( {
+	type,
+	labelPosition,
+}: {
+	type: 'default' | 'regular' | 'panel';
+	labelPosition: 'default' | 'top' | 'side' | 'none';
+} ) => {
+	const form = useMemo(
+		() => ( {
+			type,
+			labelPosition,
+			fields: [
+				'title',
+				'description',
+				'type',
+				'isPlanet',
+				'satellites',
+				'date',
+			],
+		} ),
+		[ type, labelPosition ]
+	) as Form;
+
+	const [ view, setView ] = useState< View >( {
+		type: 'table' as const,
+		search: '',
+		page: 1,
+		perPage: 10,
+		layout: {},
+		filters: [],
+		titleField: 'title',
+		descriptionField: 'description',
+		mediaField: 'image',
+		fields: [ 'type', 'isPlanet', 'satellites', 'categories', 'date' ],
+	} );
+
+	const [ selectedIds, setSelectedIds ] = useState< number[] >( [] );
+	const [ modifiedData, setModifiedData ] = useState< SpaceObject[] >( data );
+
+	const { data: shownData, paginationInfo } = useMemo( () => {
+		return filterSortAndPaginate( modifiedData, view, fields );
+	}, [ modifiedData, view ] );
+
+	let selectedItem =
+		( selectedIds.length === 1 &&
+			shownData.find( ( item ) => item.id === selectedIds[ 0 ] ) ) ||
+		null;
+
+	return (
+		<HStack spacing={ 4 } alignment="stretch" style={ { height: '80vh' } }>
+			<div style={ { flex: 2, minWidth: 0 } }>
path: packages/dataviews/src/components/dataviews-dataform-story/index.story.tsx
user: login: ntsekouras
body: Nit: it's weird to see HStack above and divs here with `flex` styles.
created_at: 2025-05-15T11:37:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,174 @@
+/**
+ * WordPress dependencies
+ */
+import { useState, useMemo } from '@wordpress/element';
+import {
+	__experimentalHStack as HStack,
+	__experimentalVStack as VStack,
+	Card,
+	CardHeader,
+	CardBody,
+} from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import DataViews from '../dataviews/index';
+import DataForm from '../dataform/index';
+import {
+	actions,
+	data,
+	fields,
+	type SpaceObject,
+} from '../dataviews/stories/fixtures';
+import { filterSortAndPaginate } from '../../filter-and-sort-data-view';
+import type { View, Form } from '../../types';
+
+const meta = {
+	title: 'DataViews/DataViewsAndDataForm',
+	component: DataForm,
path: packages/dataviews/src/components/dataviews-dataform-story/index.story.tsx
user: login: oandregal
body: > I don't know what's the best way to implement a story that uses two components, but here it's like is part of DataForm and shows those controls etc..

Yeah, I used `DataForm` here because that's the one that I want to control with the args.

> Do we want controls for such an example? If they are irrelevant we could hide them probably.

I want to have them so can test DataForm in different configurations.

> Related to that, should we add this story as part of the DataViews stories maybe?

I thought to create a new one to highlight the use of both together and that can grow with more use cases.
created_at: 2025-05-15T15:03:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,174 @@
+/**
+ * WordPress dependencies
+ */
+import { useState, useMemo } from '@wordpress/element';
+import {
+	__experimentalHStack as HStack,
+	__experimentalVStack as VStack,
+	Card,
+	CardHeader,
+	CardBody,
+} from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import DataViews from '../dataviews/index';
+import DataForm from '../dataform/index';
+import {
+	actions,
+	data,
+	fields,
+	type SpaceObject,
+} from '../dataviews/stories/fixtures';
+import { filterSortAndPaginate } from '../../filter-and-sort-data-view';
+import type { View, Form } from '../../types';
+
+const meta = {
+	title: 'DataViews/DataViewsAndDataForm',
+	component: DataForm,
+	argTypes: {
+		type: {
+			control: { type: 'select' },
+			description:
+				'Chooses the default layout of each field. "regular" is the default layout.',
+			options: [ 'default', 'regular', 'panel' ],
+		},
+		labelPosition: {
+			control: { type: 'select' },
+			description: 'Chooses the label position of the layout.',
+			options: [ 'default', 'top', 'side', 'none' ],
+		},
+	},
+} as const;
+export default meta;
+
+const defaultLayouts = {
+	table: {},
+	grid: {},
+	list: {},
+};
+
+export const Default = ( {
+	type,
+	labelPosition,
+}: {
+	type: 'default' | 'regular' | 'panel';
+	labelPosition: 'default' | 'top' | 'side' | 'none';
+} ) => {
+	const form = useMemo(
+		() => ( {
+			type,
+			labelPosition,
+			fields: [
+				'title',
+				'description',
+				'type',
+				'isPlanet',
+				'satellites',
+				'date',
+			],
+		} ),
+		[ type, labelPosition ]
+	) as Form;
+
+	const [ view, setView ] = useState< View >( {
+		type: 'table' as const,
+		search: '',
+		page: 1,
+		perPage: 10,
+		layout: {},
+		filters: [],
+		titleField: 'title',
+		descriptionField: 'description',
+		mediaField: 'image',
+		fields: [ 'type', 'isPlanet', 'satellites', 'categories', 'date' ],
+	} );
+
+	const [ selectedIds, setSelectedIds ] = useState< number[] >( [] );
+	const [ modifiedData, setModifiedData ] = useState< SpaceObject[] >( data );
+
+	const { data: shownData, paginationInfo } = useMemo( () => {
+		return filterSortAndPaginate( modifiedData, view, fields );
+	}, [ modifiedData, view ] );
+
+	let selectedItem =
+		( selectedIds.length === 1 &&
+			shownData.find( ( item ) => item.id === selectedIds[ 0 ] ) ) ||
+		null;
+
+	return (
+		<HStack spacing={ 4 } alignment="stretch" style={ { height: '80vh' } }>
+			<div style={ { flex: 2, minWidth: 0 } }>
path: packages/dataviews/src/components/dataviews-dataform-story/index.story.tsx
user: login: oandregal
body: I've pushed a simplification. Going to merge given I've got upcoming AFK but happy to iterate after merge.
created_at: 2025-05-16T07:29:59Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-15T11:12:16Z
body: <details>
<summary>Calypso Live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-144564">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-144564%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-144564">https://calypso.live?image=registry.a8c.com/calypso/app:build-144564</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Jetpack Cloud live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-144564&env=jetpack">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-144564%26env%3Djetpack%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-144564&env=jetpack">https://calypso.live?image=registry.a8c.com/calypso/app:build-144564&env=jetpack</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Automattic for Agencies live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-144564&env=a8c-for-agencies">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-144564%26env%3Da8c-for-agencies%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-144564&env=a8c-for-agencies">https://calypso.live?image=registry.a8c.com/calypso/app:build-144564&env=a8c-for-agencies</a>
</td>
</tr>
</table>
</details>
<!--calypso-live-watermark:apr@v1-->
author_association: NONE
user: login: matticbot
created_at: 2025-05-15T11:20:54Z
body: <!-- icfy-watermark: c52822 -->
This PR does not affect the size of JS and CSS bundles shipped to the user's browser.

Generated by performance advisor bot at [iscalypsofastyet.com](http://iscalypsofastyet.com).
author_association: CONTRIBUTOR
user: login: matticbot
created_at: 2025-05-16T07:44:58Z
body: **This PR modifies the release build for the following Calypso Apps:**

_For info about this notification, see here: PCYsg-OT6-p2_

* help-center

To test WordPress.com changes, run `install-plugin.sh $pluginSlug add/dataviews-dataform-story` on your sandbox.
<!--calypso-app-artifacts-watermark:apr@v1-->
author_association: CONTRIBUTOR