From 4e82ee357410ec00e1c4ee225d14842b8e99c8da Mon Sep 17 00:00:00 2001 From: Sven van de Scheur Date: Thu, 17 Oct 2024 16:22:07 +0200 Subject: [PATCH] :memo: - docs: update DataGrid/ListTemplate stories to work with updated Storybook --- .storybook/fixtures/products.ts | 112 ++++ .../data/datagrid/datagrid.stories.tsx | 558 ++++++------------ src/templates/list/list.stories.tsx | 80 +-- 3 files changed, 300 insertions(+), 450 deletions(-) create mode 100644 .storybook/fixtures/products.ts diff --git a/.storybook/fixtures/products.ts b/.storybook/fixtures/products.ts new file mode 100644 index 00000000..bbd8ad0d --- /dev/null +++ b/.storybook/fixtures/products.ts @@ -0,0 +1,112 @@ +export const FIXTURE_PRODUCTS = [ + { + id: 1, + name: "Wireless Headphones", + description: "High-quality wireless headphones with noise cancellation.", + url: "https://www.example.com/products/1", + price: 99.99, + stock: 50, + category: "Electronics", + isAvailable: true, + releaseDate: "2023-01-15", // YYYY-MM-DD format + }, + { + id: 2, + name: "Smartwatch", + description: "A smartwatch with fitness tracking and notifications.", + url: "https://www.example.com/products/2", + price: 199.99, + stock: 30, + category: "Wearables", + isAvailable: false, + releaseDate: "2023-02-20", + }, + { + id: 3, + name: "Laptop Stand", + description: "An ergonomic laptop stand to improve posture while working.", + url: "https://www.example.com/products/3", + price: 39.99, + stock: 100, + category: "Accessories", + isAvailable: true, + releaseDate: "2023-03-10", + }, + { + id: 4, + name: "Bluetooth Speaker", + description: "Portable Bluetooth speaker with rich sound quality.", + url: "https://www.example.com/products/4", + price: 49.99, + stock: 75, + category: "Audio", + isAvailable: true, + releaseDate: "2023-04-25", + }, + { + id: 5, + name: "4K Monitor", + description: "High-resolution 4K monitor for stunning visuals.", + url: "https://www.example.com/products/5", + price: 299.99, + stock: 20, + category: "Monitors", + isAvailable: false, + releaseDate: "2023-05-15", + }, + { + id: 6, + name: "Wireless Mouse", + description: "Ergonomic wireless mouse with customizable buttons.", + url: "https://www.example.com/products/6", + price: 29.99, + stock: 150, + category: "Accessories", + isAvailable: true, + releaseDate: "2023-06-30", + }, + { + id: 7, + name: "Gaming Keyboard", + description: "Mechanical gaming keyboard with RGB lighting.", + url: "https://www.example.com/products/7", + price: 89.99, + stock: 60, + category: "Gaming", + isAvailable: false, + releaseDate: "2023-07-18", + }, + { + id: 8, + name: "External Hard Drive", + description: "1TB external hard drive for data storage.", + url: "https://www.example.com/products/8", + price: 69.99, + stock: 40, + category: "Storage", + isAvailable: true, + releaseDate: "2023-08-05", + }, + { + id: 9, + name: "Phone Case", + description: "Durable phone case with shock absorption.", + url: "https://www.example.com/products/9", + price: 19.99, + stock: 200, + category: "Accessories", + isAvailable: true, + releaseDate: "2023-09-12", + }, + { + id: 10, + name: "Smart TV", + description: "65-inch smart TV with 4K resolution and streaming capabilities.", + url: "https://www.example.com/products/10", + price: 599.99, + stock: 10, + category: "Electronics", + isAvailable: false, + releaseDate: "2023-10-01", + }, +]; diff --git a/src/components/data/datagrid/datagrid.stories.tsx b/src/components/data/datagrid/datagrid.stories.tsx index 8932b510..96674958 100644 --- a/src/components/data/datagrid/datagrid.stories.tsx +++ b/src/components/data/datagrid/datagrid.stories.tsx @@ -1,9 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import * as React from "react"; -import { useEffect, useState } from "react"; -import { SerializedFormData } from "../../../lib"; -import { AttributeData } from "../../../lib/data/attributedata"; +import { FIXTURE_PRODUCTS } from "../../../../.storybook/fixtures/products"; import { Button } from "../../button"; import { Page } from "../../layout"; import { DataGrid } from "./datagrid"; @@ -26,8 +24,19 @@ const meta: Meta = { export default meta; type Story = StoryObj; +/** + * Typical example: a subset of `fields` from `objectList` representing one of + * multiple pages. + * + * `fields` prop accepts an array of either `string` or `TypedField` values, + * Please refer to `TypedField` for advanced usage. + */ export const DataGridComponent = { args: { + title: "Products", + objectList: FIXTURE_PRODUCTS, + fields: ["name", "category", "price", "stock", "isAvailable"], + urlFields: ["url"], paginatorProps: { count: 100, page: 1, @@ -40,94 +49,22 @@ export const DataGridComponent = { { label: 50 }, ], }, - objectList: [ - { - url: "https://www.example.com", - Omschrijving: "Afvalpas vervangen", - Zaaktype: "https://www.example.com", - Versie: 2, - Opmerkingen: null, - Actief: false, - Toekomstig: false, - Concept: true, - }, - { - url: "https://www.example.com", - Omschrijving: "Erfpacht wijzigen", - Zaaktype: "https://www.example.com", - Versie: 4, - Opmerkingen: null, - Actief: true, - Toekomstig: true, - Concept: true, - }, - { - url: "https://www.example.com", - Omschrijving: "Dakkapel vervangen", - Zaaktype: "https://www.example.com", - Versie: 1, - Opmerkingen: null, - Actief: false, - Toekomstig: false, - Concept: false, - }, - { - url: "https://www.example.com", - Omschrijving: "Dakkapel vervangen", - Zaaktype: "https://www.example.com", - Versie: 4, - Opmerkingen: null, - Actief: true, - Toekomstig: true, - Concept: true, - }, - { - url: "https://www.example.com", - Omschrijving: "Erfpacht wijzigen", - Zaaktype: "https://www.example.com", - Versie: 2, - Opmerkingen: null, - Actief: false, - Toekomstig: false, - Concept: true, - }, - { - url: "https://www.example.com", - Omschrijving: "Dakkapel vervangen", - Zaaktype: "https://www.example.com", - Versie: 4, - Opmerkingen: null, - Actief: true, - Toekomstig: true, - Concept: true, - }, - { - url: "https://www.example.com", - Omschrijving: "Erfpacht wijzigen", - Zaaktype: "https://www.example.com", - Versie: 1, - Opmerkingen: null, - Actief: false, - Toekomstig: false, - Concept: false, - }, - { - url: "https://www.example.com", - Omschrijving: "Dakkapel vervangen", - Zaaktype: "https://www.example.com", - Versie: 1, - Opmerkingen: null, - Actief: false, - Toekomstig: false, - Concept: false, - }, - ], - title: "Posts", - urlFields: ["url"], }, }; -export const DecoratedDataGrid: Story = { +/** + * Minimal example with only required props showing a simple table. + */ +export const MinimalExample = { + args: { + objectList: FIXTURE_PRODUCTS, + }, +}; + +/** + * pass `true` to `decorate` prop to use value decoration (see `Value` component). + */ +export const Decorated: Story = { ...DataGridComponent, args: { ...DataGridComponent.args, @@ -135,142 +72,84 @@ export const DecoratedDataGrid: Story = { }, }; -export const SortableDataGrid: Story = { +/** + * Pass `true` to `editable` prop to allow values to be edited, the exact fields + * to be editable can be controlled by passing `TypedField` items to the `fields` + * prop. + */ +export const EditableRows: Story = { ...DataGridComponent, args: { ...DataGridComponent.args, - sort: true, + fields: ["name", "category", "price", "stock", "isAvailable"], + editable: true, + }, + argTypes: { + onEdit: { action: "onEdit" }, }, }; -export const SortedDataGrid: Story = { +/** + * Pass `true` to `fieldsSelectable` prop to allow fields to be selectable, the + * default fields be active can be controlled by passing `TypedField` items to + * the `fields` prop. + */ +export const FieldsSelectable: Story = { ...DataGridComponent, args: { ...DataGridComponent.args, - sort: "Versie", + fields: [ + { name: "id", type: "number", active: false }, + "name", + "category", + "price", + "stock", + "isAvailable", + { name: "releaseDate", type: "date", active: false }, + ], + fieldsSelectable: true, + }, + argTypes: { + onFieldsChange: { action: "onFieldsChange" }, }, }; -export const JSONPlaceholderExample: Story = { +/** + * Pass `true` to `filterable` prop to allow values to be filtered, the exact fields + * to be filterable can be controlled by passing `TypedField` items to the `fields` + * prop. + */ +export const Filterable: Story = { + ...DataGridComponent, args: { - objectList: [], - showPaginator: true, - sort: true, - title: "Posts", + ...DataGridComponent.args, + filterable: true, + fields: [ + "name", + "category", + { name: "price", type: "number", filterable: false }, + { name: "stock", type: "number", filterable: false }, + { name: "isAvailable", type: "boolean" }, + ], }, - render: (args) => { - const [filterState, setFilterState] = useState(); - const [loading, setLoading] = useState(false); - const [page, setPage] = useState(args.paginatorProps?.page || 1); - const [pageSize, setPageSize] = useState(args.pageSize || 10); - const [objectList, setObjectList] = useState([]); - const [sort, setSort] = useState(""); - - /** - * Fetches object from jsonplaceholder.typicode.com. - */ - useEffect(() => { - setLoading(true); - const abortController = new AbortController(); - const sortKey = sort.replace(/^-/, ""); - const sortDirection = sort.startsWith("-") ? "desc" : "asc"; - - const filters = filterState - ? Object.fromEntries( - Object.entries(filterState) - .filter(([, value]) => value) - .map(([key, value]) => [key, String(value)]), - ) - : {}; - const searchParams = new URLSearchParams(filters); - - // Process sorting and pagination locally in place for demonstration purposes. - fetch( - `https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=${pageSize}&_sort=${sortKey}&_order=${sortDirection}&${searchParams}`, - { - signal: abortController.signal, - }, - ) - .then((response) => response.json()) - .then((data: AttributeData[]) => { - setObjectList(data); - setLoading(false); - }); - - return () => { - abortController.abort(); - setLoading(false); - }; - }, [filterState, page, pageSize, sort]); - - return ( - 0 - ? [objectList[1], objectList[3], objectList[4]] - : undefined - : undefined) - } - equalityChecker={args.equalityChecker} - onPageChange={setPage} - onPageSizeChange={setPageSize} - onSort={(field) => setSort(field)} - onEdit={async (rowData: SerializedFormData) => { - setLoading(true); - await fetch( - `https://jsonplaceholder.typicode.com/posts/${rowData.id}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(rowData), - }, - ); - const index = objectList.findIndex( - (r) => Number(r.id) === rowData.id, - ); - const newObjectList = [...objectList]; - newObjectList[index] = rowData as AttributeData; - setObjectList(newObjectList); - setLoading(false); - }} - onFilter={(data) => { - setPage(1); - setFilterState(data); - }} - /> - ); + argTypes: { + onFilter: { action: "onFilter" }, }, }; -export const SelectableRows: Story = { - ...JSONPlaceholderExample, +/** + * pass a `true` to `selectable` to allow selecting rows. + */ +export const Selectable: Story = { + ...DataGridComponent, args: { - ...JSONPlaceholderExample.args, - fields: ["userId", "id", "title"], + ...DataGridComponent.args, selectable: true, allowSelectAll: false, selectionActions: [ { - children: "Aanmaken", - name: "create", + children: "Buy!", + name: "order", onClick: ({ detail }) => { // @ts-expect-error - TODO: type should be fixed alert(`${detail.length} items selected.`); @@ -285,222 +164,155 @@ export const SelectableRows: Story = { }, }; -export const SelectableRowsWithCustomMatchFunction: Story = { - ...JSONPlaceholderExample, +/** + * Pass a function `(item1: AttributeData, item2: AttributeData) => bool`) to the `equalityChecker` prop to allow items + * in `selected` to be matched to item in `objectList`. If `equalityChecker` is omitted: strict checking + * (`item1 === item2`) is used to determine a match. + */ +export const SelectionMatchEqualityChecker: Story = { + ...Selectable, args: { - ...JSONPlaceholderExample.args, - fields: ["userId", "id", "title"], - selectable: true, - selectionActions: [ - { - children: "Aanmaken", - name: "create", - onClick: ({ detail }) => { - // @ts-expect-error: TODO: type should be fixed - alert(`${detail.length} items selected.`); - }, - }, - ], + ...Selectable.args, selected: [ { - userId: 1, id: 1, - title: - "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", - body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto", }, { - userId: 1, id: 5, - title: "nesciunt quas odio", - body: "repudiandae veniam quaerat sunt sed\nalias aut fugiat sit autem sed est\nvoluptatem omnis possimus esse voluptatibus quis\nest aut tenetur dolor neque", }, ], - equalityChecker: (item1, item2) => item1.id === item2.id, - }, - argTypes: { - onSelect: { action: "onSelect" }, - onSelectionChange: { action: "onSelectionChange" }, + equalityChecker: (item1, item2) => item1?.id === item2?.id, }, }; -export const EditableRows: Story = { - ...JSONPlaceholderExample, +/** + * pass `true` to `sort` prop to use sorting, sorting will be performed locally + * if no `onSort` prop is passed. + */ +export const Sortable: Story = { + ...DataGridComponent, args: { - ...JSONPlaceholderExample.args, - // editable: true, - fields: [ - { - type: "number", - name: "userId", - options: [ - { label: 1, value: 1 }, - { label: 2, value: 2 }, - ], - editable: true, - }, - { name: "id", type: "number", editable: false }, - "title", - { - type: "boolean", - name: "published", - }, - ], - }, - argTypes: { - onEdit: { action: "onEdit" }, + ...DataGridComponent.args, + sort: true, }, + // Don't set argTypes.onSort as it will result in a onSort prop being passed. }; -export const FilterableRows: Story = { - ...JSONPlaceholderExample, +/** + * pass a `string` to `sort` (optionally prefix by `-` to invert sorting) prop + * to set a predefined order. + */ +export const Sorted: Story = { + ...Sortable, args: { - ...JSONPlaceholderExample.args, - filterable: true, - fields: [ - { - type: "number", - name: "userId", - options: [ - { label: 1, value: 1 }, - { label: 2, value: 2 }, - ], - }, - { name: "id", type: "number", editable: false }, - "title", - { - type: "boolean", - name: "published", - }, - ], + ...Sortable.args, + sort: "-price", }, }; -export const FieldsSelectable: Story = { - ...JSONPlaceholderExample, +export const FilterableSelectableSortable: Story = { args: { - ...JSONPlaceholderExample.args, - filterable: true, + objectList: FIXTURE_PRODUCTS, fields: [ + "name", + "category", + { name: "price", type: "number", filterable: false }, + { name: "stock", type: "number", filterable: false }, + { name: "isAvailable", type: "boolean" }, + ], + filterable: true, + selectable: true, + selected: [ { - type: "number", - name: "userId", - options: [ - { label: 1, value: 1 }, - { label: 2, value: 2 }, - ], + id: 1, }, - { name: "id", type: "number", editable: false }, - "title", { - active: false, - type: "boolean", - name: "published", + id: 5, }, ], - fieldsSelectable: true, + equalityChecker: (item1, item2) => item1?.id === item2?.id, + allowSelectAll: true, + selectionActions: [ + { + children: "Buy!", + name: "order", + onClick: ({ detail }) => { + // @ts-expect-error - TODO: type should be fixed + alert(`${detail.length} items selected.`); + }, + }, + ], + sort: "price", }, argTypes: { - onFieldsChange: { action: "onFieldsChange" }, + onFilter: { action: "onFilter" }, + onSelect: { action: "onSelect" }, + onSelectionChange: { action: "onSelectionChange" }, + onSelectAllPages: { action: "onSelectAllPages" }, + onSort: { action: "onSort" }, }, }; -export const DateRangeFilter: Story = { +/** + * Pass `date` as `type` field on `TypedField` on filterable DataGrid to allow + * specifying a specific date. + */ +export const DateFilter: Story = { + ...Filterable, + args: { + ...Filterable.args, + fields: [ + "name", + "category", + "price", + "stock", + "isAvailable", + { name: "releaseDate", type: "date" }, + ], + }, argTypes: { onFilter: { action: "onFilter", }, }, +}; + +/** + * Pass `daterange` as `type` field on `TypedField` on filterable DataGrid to + * allow specifying a start and end date. + */ +export const DateRangeFilter: Story = { + ...Filterable, args: { + ...Filterable.args, fields: [ - { - name: "firstName", - type: "string", - }, - { - name: "lastName", - type: "string", - }, - { - name: "dateOfBirth", - type: "daterange", - }, + "name", + "category", + "price", + "stock", + "isAvailable", + { name: "releaseDate", type: "daterange" }, ], - filterable: true, - filterTransform: (data) => { - const { dateOfBirth, ..._data } = data; - const [dateOfBirth__gte = "", dateOfBirth__lte = ""] = - String(dateOfBirth).split("/"); - return { dateOfBirth__gte, dateOfBirth__lte, ..._data }; + }, + argTypes: { + onFilter: { + action: "onFilter", }, - objectList: [ - { - firstName: "Albert", - lastName: "Einstein", - dateOfBirth: "1879-03-14", - }, - { - firstName: "Marie", - lastName: "Curie", - dateOfBirth: "1867-11-07", - }, - { - firstName: "Martin", - lastName: "Luther King Jr.", - dateOfBirth: "1929-01-15", - }, - { - firstName: "Nelson", - lastName: "Mandela", - dateOfBirth: "1918-07-18", - }, - { - firstName: "Isaac", - lastName: "Newton", - dateOfBirth: "1643-01-04", - }, - { - firstName: "Mahatma", - lastName: "Gandhi", - dateOfBirth: "1869-10-02", - }, - { - firstName: "Ada", - lastName: "Lovelace", - dateOfBirth: "1815-12-10", - }, - { - firstName: "Leonardo", - lastName: "da Vinci", - dateOfBirth: "1452-04-15", - }, - { - firstName: "Galileo", - lastName: "Galilei", - dateOfBirth: "1564-02-15", - }, - { - firstName: "Charles", - lastName: "Darwin", - dateOfBirth: "1809-02-12", - }, - ], }, }; export const JSXAsValue: Story = { + ...DataGridComponent, args: { - objectList: [ - { - Id: 1, - Name: "Some product", - action: , - }, - { - Id: 2, - Name: "Some other product", - action: , - }, - ], + ...DataGridComponent.args, + fields: ["name", "category", "price", "stock", "isAvailable", "action"], + objectList: DataGridComponent.args.objectList.map((item) => ({ + ...item, + action: ( + + ), + })), }, }; diff --git a/src/templates/list/list.stories.tsx b/src/templates/list/list.stories.tsx index b086090f..01488326 100644 --- a/src/templates/list/list.stories.tsx +++ b/src/templates/list/list.stories.tsx @@ -1,9 +1,8 @@ import type { Meta, StoryObj } from "@storybook/react"; import * as React from "react"; -import { useEffect, useState } from "react"; -import { Badge, Outline } from "../../components"; -import { AttributeData } from "../../lib/data/attributedata"; +import { Badge, DataGridProps, Outline } from "../../components"; +import { FilterableSelectableSortable } from "../../components/data/datagrid/datagrid.stories"; import { ListTemplate } from "./list"; const meta: Meta = { @@ -16,23 +15,7 @@ type Story = StoryObj; export const listTemplate: Story = { args: { - dataGridProps: { - pageSize: 50, - objectList: [], - showPaginator: true, - sort: true, - title: "List template", - fields: ["userId", "id", "title", { active: false, name: "body" }], - filterable: true, - fieldsSelectable: true, - pageSizeOptions: [ - { label: 10 }, - { label: 20 }, - { label: 30 }, - { label: 40 }, - { label: 50 }, - ], - }, + dataGridProps: FilterableSelectableSortable.args as DataGridProps, breadcrumbItems: [ { label: "Home", href: "/" }, { label: "Templates", href: "#" }, @@ -45,63 +28,6 @@ export const listTemplate: Story = { { children: , title: "Uitloggen" }, ], }, - render: (args) => { - const [loading, setLoading] = useState(false); - const [page, setPage] = useState( - args.dataGridProps.paginatorProps?.page || 1, - ); - const [pageSize, setPageSize] = useState( - args.dataGridProps.pageSize || 10, - ); - const [objectList, setObjectList] = useState([]); - const [sort, setSort] = useState(""); - - /** - * Fetches object from jsonplaceholder.typicode.com. - */ - useEffect(() => { - setLoading(true); - const abortController = new AbortController(); - const sortKey = sort.replace(/^-/, ""); - const sortDirection = sort.startsWith("-") ? "desc" : "asc"; - - // Process sorting and pagination locally in place for demonstration purposes. - fetch( - `https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=${pageSize}&_sort=${sortKey}&_order=${sortDirection}`, - { - signal: abortController.signal, - }, - ) - .then((response) => response.json()) - .then((data: AttributeData[]) => { - setObjectList(data); - setLoading(false); - }); - - return () => { - abortController.abort(); - setLoading(false); - }; - }, [page, pageSize, sort]); - - return ( - setSort(field), - }} - /> - ); - }, }; export const WithSidebar: Story = {