Skip to content

Commit ede02f2

Browse files
committed
Add dynamic bulk action toolbar with long descriptive labels and improve styles in DataViews
1 parent d478254 commit ede02f2

3 files changed

Lines changed: 250 additions & 103 deletions

File tree

src/components/wordpress/DataViews.stories.tsx

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Meta, StoryFn } from "@storybook/react";
22
import { SlotFillProvider } from "@wordpress/components";
3-
import { Archive, Eye, Pencil, Trash2, UserCheck, Users, UserX } from "lucide-react";
3+
import { Archive, Ban, CheckCircle, Eye, Mail, Pencil, Trash2, UserCheck, Users, UserX } from "lucide-react";
44
import React, { useState } from "react";
55
import { Badge, Input } from "../ui";
66
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
@@ -1034,6 +1034,121 @@ export const FixedWidthColumns: StoryFn = () => {
10341034
);
10351035
};
10361036
FixedWidthColumns.storyName = "Fixed Width Columns";
1037+
/** Demonstrates how the bulk action toolbar handles many actions with long descriptive labels. Select rows to see the toolbar. */
1038+
export const LargeTextBulkActions: StoryFn = () => {
1039+
const [view, setView] = useState<DataViewState>(createDefaultView(["name", "email", "status", "role", "joinedAt"]));
1040+
const [selection, setSelection] = useState<string[]>([]);
1041+
const [users, setUsers] = useState(allUsers);
1042+
1043+
const paginatedData = paginateData(users, view);
1044+
1045+
const bulkActions: DataViewAction<User>[] = [
1046+
{
1047+
id: "approve-selected",
1048+
label: "Approve Selected Users",
1049+
icon: <CheckCircle size={16} />,
1050+
supportsBulk: true,
1051+
callback: async (items) => {
1052+
await new Promise((resolve) => setTimeout(resolve, 1000));
1053+
alert(`Approved: ${items.map(i => i.name).join(", ")}`);
1054+
},
1055+
},
1056+
{
1057+
id: "send-notification",
1058+
label: "Send Email Notification",
1059+
icon: <Mail size={16} />,
1060+
supportsBulk: true,
1061+
callback: async (items) => {
1062+
await new Promise((resolve) => setTimeout(resolve, 1000));
1063+
alert(`Notification sent to: ${items.map(i => i.email).join(", ")}`);
1064+
},
1065+
},
1066+
{
1067+
id: "suspend-accounts",
1068+
label: "Suspend User Accounts",
1069+
icon: <Ban size={16} />,
1070+
isDestructive: true,
1071+
supportsBulk: true,
1072+
confirmTitle: "Suspend User Accounts",
1073+
confirmMessage: "Suspended users will lose access to their accounts immediately. You can reactivate them later.",
1074+
confirmButtonLabel: "Yes, Suspend All",
1075+
cancelButtonLabel: "Keep Active",
1076+
callback: async (items) => {
1077+
await new Promise((resolve) => setTimeout(resolve, 1500));
1078+
alert(`Suspended: ${items.map(i => i.name).join(", ")}`);
1079+
},
1080+
},
1081+
{
1082+
id: "archive-permanently",
1083+
label: "Archive and Remove from List",
1084+
icon: <Archive size={16} />,
1085+
isDestructive: true,
1086+
supportsBulk: true,
1087+
confirmTitle: "Archive Users Permanently",
1088+
confirmMessage: "Archived users will be removed from all active lists and moved to the archive storage.",
1089+
confirmButtonLabel: "Archive Permanently",
1090+
callback: async (items) => {
1091+
await new Promise((resolve) => setTimeout(resolve, 1500));
1092+
const ids = new Set(items.map(i => i.id));
1093+
setUsers(prev => prev.filter(u => !ids.has(u.id)));
1094+
setSelection([]);
1095+
},
1096+
},
1097+
{
1098+
id: "delete-permanently",
1099+
label: "Delete Permanently from System",
1100+
icon: <Trash2 size={16} />,
1101+
isDestructive: true,
1102+
supportsBulk: true,
1103+
confirmTitle: "Permanently Delete Users",
1104+
confirmMessage: "This will permanently delete the selected users and all associated data. This action cannot be undone.",
1105+
confirmButtonLabel: "Delete Forever",
1106+
callback: async (items) => {
1107+
await new Promise((resolve) => setTimeout(resolve, 1500));
1108+
const ids = new Set(items.map(i => i.id));
1109+
setUsers(prev => prev.filter(u => !ids.has(u.id)));
1110+
setSelection([]);
1111+
},
1112+
},
1113+
];
1114+
1115+
return (
1116+
<div className="p-4">
1117+
<DataViews<User>
1118+
namespace="dataviews-demo"
1119+
data={paginatedData}
1120+
fields={fields}
1121+
view={view}
1122+
onChangeView={setView}
1123+
actions={bulkActions}
1124+
selection={selection}
1125+
onChangeSelection={setSelection}
1126+
paginationInfo={{
1127+
totalItems: users.length,
1128+
totalPages: getTotalPages(users.length, view.perPage),
1129+
}}
1130+
getItemId={(item) => item.id}
1131+
/>
1132+
</div>
1133+
);
1134+
};
1135+
LargeTextBulkActions.storyName = "Large Text Bulk Actions";
1136+
LargeTextBulkActions.parameters = {
1137+
docs: {
1138+
description: {
1139+
story: `Demonstrates 5 bulk actions with long, descriptive labels to test how the bulk action toolbar handles large text. Select one or more rows to see the toolbar appear.
1140+
1141+
- **Approve Selected Users** — non-destructive bulk approval
1142+
- **Send Email Notification** — non-destructive bulk email
1143+
- **Suspend User Accounts** — destructive with custom confirmation dialog
1144+
- **Archive and Remove from List** — destructive, removes items from list
1145+
- **Delete Permanently from System** — destructive, permanently removes items
1146+
1147+
All actions support bulk selection and have async callbacks with loading states.`,
1148+
},
1149+
},
1150+
};
1151+
10371152
FixedWidthColumns.parameters = {
10381153
docs: {
10391154
description: {

src/components/wordpress/dataviews.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
import { __ } from '@wordpress/i18n';
1212
import { FileSearch, Funnel, Plus, Search, X } from 'lucide-react';
1313
import type React from 'react';
14-
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
14+
import { Fragment, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
1515
import {
1616
AlertDialog,
1717
AlertDialogAction,
@@ -237,7 +237,9 @@ const FilterItems = ({
237237
return null;
238238
}
239239
return (
240-
<div className="relative flex items-center pe-2 border rounded-md border-border **:border-0 **:shadow-none" key={id}>
240+
<div
241+
className="relative flex items-center pe-2 border rounded-md border-border **:border-0 **:shadow-none"
242+
key={id}>
241243
{field.field}
242244
<span
243245
role="button"
@@ -495,6 +497,26 @@ export function DataViews<Item>(props: DataViewsProps<Item>) {
495497
...dataViewsTableProps
496498
} = props;
497499

500+
// --- Dynamic bulk action toolbar offset based on actual thead height ---
501+
const tableContainerRef = useRef<HTMLDivElement>(null);
502+
const [theadHeight, setTheadHeight] = useState(0);
503+
504+
useLayoutEffect(() => {
505+
const container = tableContainerRef.current;
506+
if (!container) return;
507+
508+
const thead = container.querySelector('thead');
509+
if (!thead) return;
510+
511+
const observer = new ResizeObserver(([entry]) => {
512+
setTheadHeight(entry.contentRect.height);
513+
});
514+
observer.observe(thead);
515+
setTheadHeight(thead.offsetHeight);
516+
517+
return () => observer.disconnect();
518+
}, [view.type]);
519+
498520
// --- Destructive action confirmation via AlertDialog ---
499521
const [pendingDestructiveAction, setPendingDestructiveAction] = useState<{
500522
action: DataViewAction<Item> & { callback: (...args: any[]) => void };
@@ -747,6 +769,7 @@ export function DataViews<Item>(props: DataViewsProps<Item>) {
747769

748770
return (
749771
<div
772+
ref={tableContainerRef}
750773
className={cn('pui-root-dataviews', children && 'custom-layout')}
751774
id={tableNameSpace}
752775
data-filter-id={filterId}>
@@ -835,8 +858,9 @@ export function DataViews<Item>(props: DataViewsProps<Item>) {
835858
{view.type === 'table' && filteredProps?.selection?.length > 0 && (
836859
<div
837860
className={cn(
838-
'animate-in fade-in-0 slide-in-from-top-1 duration-200 transition-all ease-in-out -mb-13 flex items-center bg-background z-1 border-b px-5 h-13 justify-between border-border w-full'
839-
)}>
861+
'animate-in py-1.5 fade-in-0 slide-in-from-top-1 duration-200 transition-all ease-in-out flex items-center bg-background z-1 border-b px-6 min-h-13 justify-between border-border w-full'
862+
)}
863+
style={theadHeight ? { marginBottom: -theadHeight, minHeight: theadHeight } : undefined}>
840864
<DataViewsTable.BulkActionToolbar />
841865
</div>
842866
)}

0 commit comments

Comments
 (0)