Skip to content

Commit 9f977bd

Browse files
committed
feat: enhance FilterItems and Tabs to support item count display
1 parent 478d8ad commit 9f977bd

2 files changed

Lines changed: 179 additions & 41 deletions

File tree

src/components/wordpress/DataViews.stories.tsx

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,138 @@ export const Loading: StoryFn = () => {
596596
};
597597
Loading.storyName = "Loading State";
598598

599+
/** DataViews with only filters and search — no tabs. Click "Add Filter" to add filters. */
600+
export const FiltersOnly: StoryFn = () => {
601+
const [view, setView] = useState<DataViewState>(createDefaultView(["name", "email", "status", "role", "joinedAt"]));
602+
const [nameFilter, setNameFilter] = useState("");
603+
const [statusFilter, setStatusFilter] = useState("");
604+
const [roleFilter, setRoleFilter] = useState("");
605+
606+
let filteredUsers = [...allUsers];
607+
608+
// Apply search
609+
const searchTerm = view.search ?? "";
610+
if (searchTerm) {
611+
filteredUsers = filteredUsers.filter(user =>
612+
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
613+
user.email.toLowerCase().includes(searchTerm.toLowerCase())
614+
);
615+
}
616+
617+
if (nameFilter) {
618+
filteredUsers = filteredUsers.filter(user =>
619+
user.name.toLowerCase().includes(nameFilter.toLowerCase())
620+
);
621+
}
622+
623+
if (statusFilter) {
624+
filteredUsers = filteredUsers.filter(user => user.status === statusFilter);
625+
}
626+
627+
if (roleFilter) {
628+
filteredUsers = filteredUsers.filter(user => user.role === roleFilter);
629+
}
630+
631+
const paginatedData = paginateData(filteredUsers, view);
632+
633+
const filterFields: DataViewFilterField[] = [
634+
{
635+
id: "name",
636+
label: "Name",
637+
field: (
638+
<Input
639+
placeholder="Filter by name..."
640+
value={nameFilter}
641+
onChange={(e) => {
642+
setNameFilter(e.target.value);
643+
setView(prev => ({ ...prev, page: 1 }));
644+
}}
645+
className="w-48"
646+
/>
647+
),
648+
},
649+
{
650+
id: "status",
651+
label: "Status",
652+
field: (
653+
<Select
654+
value={statusFilter}
655+
onValueChange={(value) => {
656+
setStatusFilter(value ?? "");
657+
setView(prev => ({ ...prev, page: 1 }));
658+
}}
659+
>
660+
<SelectTrigger className="w-40">
661+
<SelectValue placeholder="Select status" />
662+
</SelectTrigger>
663+
<SelectContent>
664+
<SelectItem value="active">Active</SelectItem>
665+
<SelectItem value="inactive">Inactive</SelectItem>
666+
<SelectItem value="pending">Pending</SelectItem>
667+
</SelectContent>
668+
</Select>
669+
),
670+
},
671+
{
672+
id: "role",
673+
label: "Role",
674+
field: (
675+
<Select
676+
value={roleFilter}
677+
onValueChange={(value) => {
678+
setRoleFilter(value ?? "");
679+
setView(prev => ({ ...prev, page: 1 }));
680+
}}
681+
>
682+
<SelectTrigger className="w-40">
683+
<SelectValue placeholder="Select role" />
684+
</SelectTrigger>
685+
<SelectContent>
686+
<SelectItem value="Admin">Admin</SelectItem>
687+
<SelectItem value="Editor">Editor</SelectItem>
688+
<SelectItem value="Viewer">Viewer</SelectItem>
689+
<SelectItem value="Manager">Manager</SelectItem>
690+
</SelectContent>
691+
</Select>
692+
),
693+
},
694+
];
695+
696+
return (
697+
<div className="p-4">
698+
<DataViews<User>
699+
namespace="dataviews-demo"
700+
data={paginatedData}
701+
fields={fields}
702+
view={view}
703+
onChangeView={setView}
704+
actions={actions}
705+
paginationInfo={{
706+
totalItems: filteredUsers.length,
707+
totalPages: getTotalPages(filteredUsers.length, view.perPage),
708+
}}
709+
getItemId={(item) => item.id}
710+
filter={{
711+
fields: filterFields,
712+
onReset: () => {
713+
setNameFilter("");
714+
setStatusFilter("");
715+
setRoleFilter("");
716+
setView(prev => ({ ...prev, page: 1 }));
717+
},
718+
onFilterRemove: (filterId) => {
719+
if (filterId === "name") setNameFilter("");
720+
if (filterId === "status") setStatusFilter("");
721+
if (filterId === "role") setRoleFilter("");
722+
setView(prev => ({ ...prev, page: 1 }));
723+
},
724+
}}
725+
/>
726+
</div>
727+
);
728+
};
729+
FiltersOnly.storyName = "Filters Only";
730+
599731
/** Complete example with all features: tabs, filters, search, selection, and pagination. */
600732
export const FullFeatured: StoryFn = () => {
601733
const [view, setView] = useState<DataViewState>(createDefaultView(["name", "email", "status", "role", "joinedAt"]));
@@ -827,7 +959,7 @@ export const LayoutCustomComponent: StoryFn = () => {
827959
type: "table",
828960
search: "",
829961
page: 1,
830-
perPage: 6,
962+
perPage: 10,
831963
fields: ["name", "email", "status", "role"],
832964
});
833965

src/components/wordpress/dataviews.tsx

Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ const FilterItems = ({
213213
return null;
214214
}
215215
return (
216-
<div className="relative inline-flex items-center" key={id}>
216+
<div className="relative flex items-center gap-2" key={id}>
217217
<div className="[&>input]:pr-8 [&>select]:pr-8">{field.field}</div>
218218
<span
219219
role="button"
@@ -270,6 +270,7 @@ interface Tab {
270270
value: string;
271271
className?: string;
272272
icon?: React.ComponentType<{ className?: string }>;
273+
count?: number;
273274
disabled?: boolean;
274275
}
275276

@@ -446,41 +447,42 @@ export function DataViews<Item>(props: DataViewsProps<Item>) {
446447
}
447448
}, [activeFilterCount]);
448449

449-
const tabsWithFilterButton =
450-
filter?.fields && tabs && filter.fields.length > 0
451-
? (() => {
452-
const existing = tabs?.headerContent || tabs?.headerSlot || [];
453-
const newButton = (
454-
<button
455-
type="button"
456-
ref={setButtonRef}
457-
title="Filter"
458-
className={cn(
459-
'relative inline-flex items-center gap-2 rounded-md bg-transparent! hover:bg-transparent! px-3 py-1.5 text-sm hover:text-primary',
460-
showFilters ? 'text-primary' : 'text-muted-foreground'
461-
)}
462-
onClick={() => {
463-
if (activeFilterCount > 0) {
464-
setShowFilters((prev) => !prev);
465-
} else {
466-
setOpenSelectorSignal((s) => s + 1);
467-
}
468-
}}>
469-
<Funnel size={20} />
470-
{activeFilterCount > 0 && (
471-
<span className="absolute -top-1.5 -right-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-medium text-primary-foreground">
472-
{activeFilterCount}
473-
</span>
474-
)}
475-
</button>
476-
);
477-
478-
return {
479-
...tabs,
480-
headerContent: [...existing, newButton]
481-
};
482-
})()
483-
: tabs;
450+
const hasFilters = (filter?.fields?.length ?? 0) > 0;
451+
452+
const tabsWithFilterButton = hasFilters
453+
? (() => {
454+
const existing = tabs?.headerContent || tabs?.headerSlot || [];
455+
const newButton = (
456+
<button
457+
type="button"
458+
ref={setButtonRef}
459+
title="Filter"
460+
className={cn(
461+
'relative inline-flex items-center gap-2 rounded-md bg-transparent! hover:bg-transparent! px-3 py-1.5 text-sm hover:text-primary',
462+
showFilters ? 'text-primary' : 'text-muted-foreground'
463+
)}
464+
onClick={() => {
465+
if (activeFilterCount > 0) {
466+
setShowFilters((prev) => !prev);
467+
} else {
468+
setOpenSelectorSignal((s) => s + 1);
469+
}
470+
}}>
471+
<Funnel size={20} />
472+
{activeFilterCount > 0 && (
473+
<span className="absolute -top-1.5 -right-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-medium text-primary-foreground">
474+
{activeFilterCount}
475+
</span>
476+
)}
477+
</button>
478+
);
479+
480+
return {
481+
...tabs,
482+
headerContent: [...existing, newButton]
483+
};
484+
})()
485+
: tabs;
484486

485487
const resolvedTabsConfig = tabsWithFilterButton || tabs;
486488

@@ -502,6 +504,7 @@ export function DataViews<Item>(props: DataViewsProps<Item>) {
502504
? Math.ceil(paginationDetails.totalItems / perPage)
503505
: 0;
504506
const shouldShowPagination = (typeof explicitTotalPages === 'number' ? explicitTotalPages : computedTotalPages) > 1;
507+
const showFullWidthHeader = !tabItems.length && (search || hasFilters);
505508

506509
const tableNameSpace = kebabCase(namespace);
507510

@@ -521,7 +524,7 @@ export function DataViews<Item>(props: DataViewsProps<Item>) {
521524
<Search size={18} className="text-muted-foreground" />
522525
</InputGroupAddon>
523526
<InputGroupInput
524-
className="border! border-border!"
527+
className="border-none!"
525528
placeholder={searchPlaceholder}
526529
value={searchTerm}
527530
onChange={(event) =>
@@ -583,7 +586,10 @@ export function DataViews<Item>(props: DataViewsProps<Item>) {
583586
tab.className
584587
)}>
585588
{tab.icon && <tab.icon className="size-4" />}
586-
{tab.label}
589+
{tab.label}{' '}
590+
{tab.count !== undefined && (
591+
<span className="text-muted-foreground">({tab.count})</span>
592+
)}
587593
</TabsTrigger>
588594
))}
589595
</TabsList>
@@ -592,7 +598,7 @@ export function DataViews<Item>(props: DataViewsProps<Item>) {
592598
<div
593599
className={cn(
594600
'flex items-center gap-2',
595-
!tabItems.length && search && 'justify-end w-full py-2'
601+
showFullWidthHeader && 'justify-end w-full py-2'
596602
)}>
597603
{searchInput}
598604
{headerContent.map((node, index) => (
@@ -601,7 +607,7 @@ export function DataViews<Item>(props: DataViewsProps<Item>) {
601607
</div>
602608
</div>
603609

604-
{filter && filter.fields && filter.fields.length > 0 && (
610+
{hasFilters && (
605611
<div
606612
className={`transition-all flex w-full justify-between px-4 my-4 bg-background ${
607613
showFilters ? '' : 'hidden!'

0 commit comments

Comments
 (0)