diff --git a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx index 27d0fbde385..8421d5e1532 100644 --- a/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx +++ b/packages/styleguide/src/lib/Organisms/Lists & Tables/DataList/DataList.stories.tsx @@ -1,8 +1,19 @@ // Added because SB and TS don't play nice with each other at the moment // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck -import { DataList, DataTable, FlexBox } from '@codecademy/gamut'; +import { + Anchor, + DataList, + FillButton, + FlexBox, + List, + ListCol, + ListRow, + TableHeader, + Text, +} from '@codecademy/gamut'; import type { Meta, StoryObj } from '@storybook/react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { cols, @@ -104,21 +115,7 @@ const meta: Meta = { onRowExpand: () => {}, expandedContent: ({ row }) => ( - + Expanded content for {row.name} ), }, @@ -213,3 +210,721 @@ export const DisableContainerQuery: Story = { args: {}, render: () => , }; + +// Server-side filtering example component +const ServerSideFilteringExample = () => { + // Mock data for our example + const allCrewMembers = useMemo( + () => [ + { + id: 1, + name: 'Jean Luc Picard', + role: 'Captain', + ship: 'USS Enterprise', + species: 'Human', + status: 'Active', + }, + { + id: 2, + name: 'Wesley Crusher', + role: 'Deus Ex Machina', + ship: 'USS Enterprise', + species: 'Human/Traveler', + status: 'Transcended', + }, + { + id: 3, + name: 'Geordie LaForge', + role: 'Chief Engineer', + ship: 'USS Enterprise', + species: 'Human', + status: 'Active', + }, + { + id: 4, + name: 'Data', + role: 'Lt. Commander', + ship: 'USS Enterprise', + species: 'Android', + status: 'Active', + }, + { + id: 5, + name: 'William Riker', + role: 'First Officer', + ship: 'USS Titan', + species: 'Human', + status: 'Active', + }, + { + id: 6, + name: 'Worf', + role: 'Security Officer', + ship: 'DS9', + species: 'Klingon', + status: 'Active', + }, + ], + [] + ); + + // State management for server-side filtering + const [rows, setRows] = useState(allCrewMembers); + const [query, setQuery] = useState({ sort: {}, filter: {} }); + const [loading, setLoading] = useState(false); + const [apiCallInfo, setApiCallInfo] = useState('No filters applied yet'); + + // Mock API call function - in real implementation, replace with your actual API + const fetchFilteredData = useCallback( + async (filterQuery) => { + setLoading(true); + + // Simulate API call delay + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Build query params that would be sent to your API + const queryParams = new URLSearchParams(); + + // Add filters to query params + if (filterQuery.filter) { + Object.entries(filterQuery.filter).forEach(([key, values]) => { + if (values && values.length > 0) { + queryParams.append(`filter[${key}]`, values.join(',')); + } + }); + } + + // Add sorts to query params + if (filterQuery.sort) { + Object.entries(filterQuery.sort).forEach(([key, direction]) => { + if (direction && direction !== 'none') { + queryParams.append(`sort[${key}]`, direction); + } + }); + } + + // Show what would be sent to the API + const queryString = queryParams.toString(); + setApiCallInfo( + queryString + ? `API called with: ${queryString}` + : 'API called with no filters' + ); + + // In a real implementation, you would make an actual API call here: + // const response = await fetch(`/api/crew?${queryString}`); + // const data = await response.json(); + // return data.rows; + + // Mock server-side filtering logic (replace this with actual API response) + let filteredData = [...allCrewMembers]; + + // Apply filters + if (filterQuery.filter) { + Object.entries(filterQuery.filter).forEach(([key, values]) => { + if (values && values.length > 0) { + filteredData = filteredData.filter( + (row) => !values.includes(row[key]) + ); + } + }); + } + + // Apply sorting + if (filterQuery.sort) { + Object.entries(filterQuery.sort).forEach(([key, direction]) => { + if (direction && direction !== 'none') { + filteredData.sort((a, b) => { + const aVal = String(a[key]).toLowerCase(); + const bVal = String(b[key]).toLowerCase(); + const comparison = aVal.localeCompare(bVal); + return direction === 'asc' ? comparison : -comparison; + }); + } + }); + } + + return filteredData; + }, + [allCrewMembers] + ); + + // Handle query changes (filters and sorts) + const handleQueryChange = useCallback( + async (change) => { + let newQuery = { ...query }; + + switch (change.type) { + case 'filter': { + const { dimension, value } = change.payload; + newQuery = { + ...newQuery, + filter: { ...newQuery.filter, [dimension]: value }, + }; + break; + } + case 'sort': { + const { dimension, value } = change.payload; + newQuery = { + ...newQuery, + sort: { [dimension]: value }, + }; + break; + } + case 'reset': { + newQuery = { sort: {}, filter: {} }; + break; + } + } + + setQuery(newQuery); + + // Fetch filtered data from server + const filteredRows = await fetchFilteredData(newQuery); + setRows(filteredRows); + setLoading(false); + }, + [query, fetchFilteredData] + ); + + // Initial load + useEffect(() => { + fetchFilteredData(query) + .then((data) => { + setRows(data); + setLoading(false); + }) + .catch(() => { + setLoading(false); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Column configuration with filterable columns + const columns = [ + { + header: 'Name', + key: 'name', + size: 'lg', + type: 'header', + sortable: true, + }, + { + header: 'Rank', + key: 'role', + size: 'lg', + sortable: true, + }, + { + header: 'Ship', + key: 'ship', + size: 'lg', + sortable: true, + // Available filter options for this column + filters: ['USS Enterprise', 'USS Titan', 'DS9'], + }, + { + header: 'Species', + key: 'species', + size: 'lg', + sortable: true, + // Available filter options for this column + filters: ['Human', 'Android', 'Klingon'], + }, + { + header: 'Status', + key: 'status', + size: 'md', + fill: true, + sortable: true, + filters: ['Active', 'Transcended'], + }, + ]; + + return ( + + Server-Side Filtering Example + + This example demonstrates how to implement server-side filtering. When + you apply a filter or sort, the component calls an API endpoint with the + filter parameters instead of filtering locally. + + + {apiCallInfo} + + + + Implementation notes: +
• Replace fetchFilteredData with your actual API call +
• The onQueryChange callback receives filter/sort + changes +
+ • Pass query parameters to your API endpoint +
+ • Update rows with the filtered data from the API response +
• Use the loading prop to show loading state during + API calls +
+
+ ); +}; + +export const ServerSideFiltering: Story = { + render: () => , +}; + +// Custom expand/collapse example without chevron +const CustomExpandExample = () => { + // Expanded data with crew members + const crewData = useMemo( + () => [ + { + id: 1, + name: 'Jean Luc Picard', + role: 'Captain', + ship: 'USS Enterprise', + bio: 'An experienced Starfleet officer known for his diplomatic skills and moral integrity.', + }, + { + id: 2, + name: 'Wesley Crusher', + role: 'Acting Ensign', + ship: 'USS Enterprise', + bio: 'A young prodigy who eventually transcends to a higher plane of existence.', + }, + { + id: 3, + name: 'Geordie LaForge', + role: 'Chief Engineer', + ship: 'USS Enterprise', + bio: 'A brilliant engineer who can see with the help of his VISOR.', + }, + { + id: 4, + name: 'Data', + role: 'Lt. Commander', + ship: 'USS Enterprise', + bio: 'An android exploring what it means to be human.', + }, + ], + [] + ); + + // Track which rows are expanded + const [expandedIds, setExpandedIds] = useState([]); + + // Handler to toggle expansion + const handleToggleExpand = useCallback((rowId) => { + setExpandedIds((prev) => { + if (prev.includes(rowId)) { + return prev.filter((id) => id !== rowId); + } + return [...prev, rowId]; + }); + }, []); + + // Generate rows that include expanded content inline + const rows = useMemo(() => { + const result = []; + crewData.forEach((crew) => { + // Add the main row + result.push({ + ...crew, + rowType: 'main', + mainId: crew.id, + }); + + // If expanded, add an expanded content row + if (expandedIds.includes(crew.id)) { + result.push({ + id: `${crew.id}-expanded`, + name: crew.name, + bio: crew.bio, + role: '', + ship: '', + rowType: 'expanded', + mainId: crew.id, + }); + } + }); + return result; + }, [crewData, expandedIds]); + + // Columns with custom expand trigger and expanded content rendering + const columns = useMemo( + () => [ + { + header: 'Name', + key: 'name', + size: 'lg', + type: 'header', + render: (row) => { + if (row.rowType === 'expanded') { + return ( + + Biography + {row.bio} + + handleToggleExpand(row.mainId)} + > + Close + + { + // eslint-disable-next-line no-alert + alert(`More about ${row.name}`); + }} + > + Learn More + + + + ); + } + return ( + { + e.preventDefault(); + handleToggleExpand(row.id); + }} + > + {row.name} + + ); + }, + }, + { + header: 'Rank', + key: 'role', + size: 'lg', + }, + { + header: 'Ship', + key: 'ship', + size: 'lg', + fill: true, + }, + ], + [handleToggleExpand] + ); + + return ( + + Custom Expand/Collapse (No Chevron) + + This example shows expand/collapse without the built-in chevron button. + Click on any crew member's name to expand their bio inline. + + + + Implementation notes: +
• Don't use expandedContent or{' '} + onRowExpand props +
+ • Generate rows dynamically - insert "expanded" rows after + expanded items +
• Use a rowType field to distinguish main vs expanded + rows +
• In column render functions, check{' '} + row.rowType to render differently +
• For expanded rows, span content across the first column and + leave others empty +
+
+ ); +}; + +export const CustomExpand: Story = { + render: () => , +}; + +// List component as table in expanded content example +const ListAsTableExample = () => { + const crew = useMemo( + () => [ + { + id: 1, + name: 'Jean Luc Picard', + role: 'Captain', + ship: 'USS Enterprise', + }, + { + id: 2, + name: 'Wesley Crusher', + role: 'Acting Ensign', + ship: 'USS Enterprise', + }, + { + id: 3, + name: 'Geordie LaForge', + role: 'Chief Engineer', + ship: 'USS Enterprise', + }, + { + id: 4, + name: 'Data', + role: 'Lt. Commander', + ship: 'USS Enterprise', + }, + ], + [] + ); + + // Mock mission data for each crew member + const missionData = useMemo( + () => ({ + 1: [ + { + id: 'm1', + mission: 'First Contact with the Borg', + stardate: '42761.3', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm2', + mission: 'Diplomatic Mission to Romulus', + stardate: '43152.4', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm3', + mission: 'Rescue Operation at Wolf 359', + stardate: '44001.4', + status: 'Completed', + outcome: 'Partial Success', + }, + ], + 2: [ + { + id: 'm4', + mission: 'Training Exercise Alpha', + stardate: '42523.7', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm5', + mission: 'Assist in Engine Repairs', + stardate: '42901.3', + status: 'Completed', + outcome: 'Success', + }, + ], + 3: [ + { + id: 'm6', + mission: 'Engine Overhaul Project', + stardate: '42686.4', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm7', + mission: 'Holodeck Maintenance', + stardate: '43125.8', + status: 'In Progress', + outcome: 'Pending', + }, + { + id: 'm8', + mission: 'Warp Core Analysis', + stardate: '43349.2', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm9', + mission: 'Sensor Array Upgrade', + stardate: '43489.2', + status: 'Completed', + outcome: 'Success', + }, + ], + 4: [ + { + id: 'm10', + mission: 'Science Survey Mission', + stardate: '42761.9', + status: 'Completed', + outcome: 'Success', + }, + { + id: 'm11', + mission: 'Away Team Investigation', + stardate: '43125.8', + status: 'Completed', + outcome: 'Success', + }, + ], + }), + [] + ); + + const [expandedRows, setExpandedRows] = useState([]); + + const onRowExpand = useCallback(({ payload: { toggle, rowId } }) => { + setExpandedRows((prev) => { + if (toggle) { + return prev.filter((id) => id !== rowId); + } + return [...prev, rowId]; + }); + }, []); + + const columns = useMemo( + () => [ + { + header: 'Name', + key: 'name', + size: 'lg', + type: 'header', + }, + { + header: 'Rank', + key: 'role', + size: 'lg', + }, + { + header: 'Ship', + key: 'ship', + size: 'lg', + fill: true, + }, + ], + [] + ); + + const expandedContent = useCallback( + ({ row }) => { + const missions = missionData[row.id] || []; + + return ( + + + Mission History for {row.name} + + + + Mission + + + Stardate + + + Status + + + Outcome + + + } + id={`missions-list-${row.id}`} + variant="table" + > + {missions.map((mission) => ( + + + + {mission.mission} + + + + {mission.stardate} + + + {mission.status} + + + {mission.outcome} + + + ))} + + + ); + }, + [missionData] + ); + + return ( + + + List Component as Table in Expanded Content + + + This example shows how to use the List component with{' '} + as="table" inside expanded content. Click the + chevron to expand a crew member and see their mission history. + + + + Implementation notes: +
• Use{' '} + List as="table" variant="table" as the + container +
• Pass a header prop with TableHeader{' '} + containing ListCol components with{' '} + columnHeader prop +
• Use ListRow and ListCol for data rows +
• Set type="header" on the first column for + row headers +
• List component gives you more flexibility than DataTable for + custom layouts +
+
+ ); +}; + +export const ListAsTable: Story = { + render: () => , +};