Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add react table filter component #2515

Open
wants to merge 27 commits into
base: update/vendor-dashboard-structure
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f177dae
feat: introduce dataviews from dokan free.
MdAsifHossainNadim Dec 31, 2024
eefed6b
feat: introduce dataviews from dokan free.
MdAsifHossainNadim Jan 1, 2025
29c723d
revert: add dokan category comission js.
MdAsifHossainNadim Jan 1, 2025
b423324
update: remove category commission js updates.
MdAsifHossainNadim Jan 1, 2025
5e00deb
remove: revert component package js file.
MdAsifHossainNadim Jan 1, 2025
cb605ad
remove: dokan sidebar class from data view table.
MdAsifHossainNadim Jan 1, 2025
a3852e8
enhance: update dataviews doc.
MdAsifHossainNadim Jan 1, 2025
4915962
update: filter naming convension for dataviews property.
MdAsifHossainNadim Jan 2, 2025
f3d3cc2
remove: module federation node package from json file.
MdAsifHossainNadim Jan 2, 2025
0f89a6b
remove: example dataviews table usage component.
MdAsifHossainNadim Jan 2, 2025
c8bb6e1
remove: example dataviews table usage component.
MdAsifHossainNadim Jan 2, 2025
ec87691
update: button class name for color scheme customizer.
MdAsifHossainNadim Jan 3, 2025
68c108c
update: split webpack entries, use change-case for namespace cases, h…
MdAsifHossainNadim Jan 6, 2025
36d3b9a
Merge branch 'update/vendor-dashboard-structure' into enhance/introdu…
MdAsifHossainNadim Jan 6, 2025
f1dd3df
update: introduce utilities directory, make exporter from dokan free …
MdAsifHossainNadim Jan 7, 2025
cca9989
update: routing folder naming convesional stuff.
MdAsifHossainNadim Jan 8, 2025
6d2d608
update: add component and utilities accessor doc for dokan free.
MdAsifHossainNadim Jan 8, 2025
a8fb22b
Add router param
Aunshon Jan 14, 2025
c84d06d
Merge branch 'refs/heads/update/vendor-dashboard-structure' into enha…
MdAsifHossainNadim Jan 14, 2025
d524e67
fix: add file exists check before register components scripts.
MdAsifHossainNadim Jan 14, 2025
d6d63ed
revert: remove default config from webpack entries.
MdAsifHossainNadim Jan 14, 2025
c12dd7b
Merge branch 'refs/heads/update/vendor-dashboard-structure' into enha…
MdAsifHossainNadim Jan 15, 2025
8182a1c
Filter component and documentation added.
Aunshon Jan 17, 2025
30a1b26
Filter component and documentation added.
Aunshon Jan 17, 2025
e12ac71
Filter component and documentation added.
Aunshon Jan 17, 2025
6439152
Filter component and documentation added.
Aunshon Jan 17, 2025
aa90f8f
Merge branch 'update/vendor-dashboard-structure' into enhance/add-rea…
Aunshon Jan 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 208 additions & 0 deletions docs/frontend/filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# Filter Component

The Filter component provides a standardized way to implement filtering functionality across your application. It creates a consistent user experience by managing filter fields, filter triggers, and reset functionality.

## Features

- Unified interface for multiple filter fields
- Configurable filter and reset buttons
- Namespace support for unique identification
- Flexible field composition

## Installation

#### Dokan lite

```jsx
import Filter from '../components';
```

#### Dokan pro or external plugins

```jsx
import { Filter } from '@dokan/components';
```

## Usage

```jsx
<Filter
fields={[
<CustomerFilter key="customer_filter" {...props} />,
<DateFilter key="date_filter" {...props} />
]}
onFilter={handleFilter}
onReset={clearFilter}
showFilter={true}
showReset={true}
namespace="vendor_subscription"
/>
```

## Props

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `namespace` | `string` | Yes | Unique identifier for the filter group |
| `fields` | `ReactNode[]` | No | Array of filter field components to be rendered |
| `onFilter` | `() => void` | No | Handler function called when the filter button is clicked |
| `onReset` | `() => void` | No | Handler function called when the reset button is clicked |
| `showFilter` | `boolean` | No | Controls visibility of the filter button (default: true) |
| `showReset` | `boolean` | No | Controls visibility of the reset button (default: true) |

## Example Implementation

```jsx
import { Filter, CustomerFilter, DateFilter } from '@your-package/components';

const MyTableComponent = () => {
const [filterArgs, setFilterArgs] = useState({});
const [searchedCustomer, setSearchedCustomer] = useState(null);

const handleFilter = () => {
// Process request payload through applyFilters
const requestPayload = applyFilters('dokan_subscription_filter_request_param', {
...filterArgs,
per_page: perPage,
page: currentPage,
} );

// Make server request with processed payload
fetchFilteredData(requestPayload);
};

const clearFilter = () => {
setFilterArgs({});
setSearchedCustomer(null);
};

const handleCustomerSearch = (customer) => {
setSearchedCustomer(customer);
};

return (
<div>
<Filter
fields={[
<CustomerFilter
key="customer_filter"
filterArgs={filterArgs}
setFilterArgs={setFilterArgs}
searchedCustomer={searchedCustomer}
handleCustomerSearch={handleCustomerSearch}
/>,
<DateFilter
key="date_filter"
filterArgs={filterArgs}
setFilterArgs={setFilterArgs}
/>
]}
onFilter={handleFilter}
onReset={clearFilter}
showFilter={true}
showReset={true}
namespace="my_table_filters"
/>
{/* Table component */}
</div>
);
};
```

## Creating Custom Filter Fields

When creating custom filter field components to use with the Filter component:

1. Each field component should manage its own state
2. Field components should update the parent's filterArgs through props
3. Include a unique key prop for each field
4. Handle reset functionality through props

Example custom filter field:

```jsx
const CustomFilter = ({ filterArgs, setFilterArgs }) => {
const handleChange = (value) => {
setFilterArgs({
...filterArgs,
customField: value
});
};

return (
<input
type="text"
value={filterArgs.customField || ''}
onChange={(e) => handleChange(e.target.value)}
/>
);
};
```

## Important Implementation Notes

### Server Request Handling
Before making requests to the server in your `onFilter` handler, you must process the request payload through an `applyFilters` function. This is a critical step for maintaining consistent filter behavior across the application.

#### Filter Hook Naming Convention
The filter hook name should follow this pattern:
- Start with "dokan"
- Follow with the feature or task name
- End with "_filter_request_param"
- Must be in snake_case format

You can use the `snakeCase` utility to ensure proper formatting:

For Dokan Lite:
```jsx
import { snakeCase } from "@/utilities";
```

For Dokan Pro or other plugins:
```jsx
import { snakeCase } from "@dokan/utilities";
```

Example usage:
```jsx
const handleFilter = () => {
// Convert hook name to snake_case
const hookName = snakeCase('dokan_subscription_filter_request_param');

// Apply filters to your request payload before sending to server
const requestPayload = applyFilters(hookName, {
...filterArgs,
per_page: perPage,
page: currentPage,
} );

// Now make your server request with the processed payload
fetchData(requestPayload);
};
```

Parameters for `applyFilters`:
- First parameter: Hook name (string, must follow naming convention)
- Second parameter: Request payload (object)
- Third parameter: Namespace from Filter component props (string)

This step ensures:
- Consistent filter processing across the application
- Proper parameter formatting
- Extensibility for future filter modifications
- Standardized hook naming across the application

## Best Practices

1. Always provide unique keys for field components
2. Implement proper type checking for filter arguments
3. Handle edge cases in reset functionality
4. Use consistent naming conventions for filter arguments
5. Include error handling in filter and reset handlers

## Notes

- The Filter component is designed to work with table components but can be used in other contexts
- All filter fields should be controlled components
- The namespace prop is used internally for generating unique identifiers
- Filter and reset functionality should be implemented in the parent component
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"vue-wp-list-table": "^1.3.0",
"vue2-daterange-picker": "^0.6.8",
"vuedraggable": "^2.24.3",
"wp-readme-to-markdown": "^1.0.1"
"wp-readme-to-markdown": "^1.0.1",
"tailwind-merge": "^2.6.0"
},
"dependencies": {
"@getdokan/dokan-ui": "^1.0.18",
Expand Down
78 changes: 78 additions & 0 deletions src/components/Filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Button } from '@getdokan/dokan-ui';
import { __ } from '@wordpress/i18n';
import { twMerge } from 'tailwind-merge';
// @ts-ignore
// eslint-disable-next-line import/no-unresolved
import { snakeCase, kebabCase } from '../utilities';

interface FilterProps {
/** Namespace for the filter, used to generate unique IDs */
namespace: string;
/** Array of React nodes representing the filter fields */
fields?: React.ReactNode[];
/** Whether to show the reset button */
showReset?: boolean;
/** Whether to show the filter button */
showFilter?: boolean;
/** Callback function to handle filter action */
onFilter?: () => void;
/** Callback function to handle reset action */
onReset?: () => void;
/** Additional class names for the filter container */
className?: string;
}

const Filter = ( {
namespace = '',
fields = [],
showReset = true,
showFilter = true,
onFilter = () => {},
onReset = () => {},
className = '',
}: FilterProps ) => {
const snakeCaseNamespace = snakeCase( namespace );
const filterId = `dokan_${ snakeCaseNamespace }_filters`;

// @ts-ignore
const filteredFields = wp.hooks.applyFilters( filterId, fields );

return (
<div
className={ twMerge(
'flex gap-4 flex-row flex-wrap pb-5 items-end dokan-dashboard-filters',
className
) }
id={ kebabCase( filterId ) }
data-filter-id={ filterId }
>
{ filteredFields.map( ( fieldNode: React.ReactNode, index ) => {
return (
<div className="dokan-dashboard-filter-item" key={ index }>
{ fieldNode }
</div>
);
} ) }

{ showFilter && (
<Button
color="primary"
className="bg-dokan-btn hover:dokan-btn-hover h-10"
label={ __( 'Filter', 'dokan' ) }
onClick={ onFilter }
/>
) }

{ showReset && (
<Button
color="primary"
className="bg-dokan-btn hover:dokan-btn-hover h-10"
label={ __( 'Reset', 'dokan' ) }
onClick={ onReset }
/>
) }
</div>
);
};

export default Filter;
2 changes: 2 additions & 0 deletions src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export {
filterSortAndPaginate,
isItemValid
} from '@wordpress/dataviews/wp';

export { default as Filter } from './Filter';
46 changes: 46 additions & 0 deletions src/hooks/ViewportDimensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useState, useEffect, useCallback } from '@wordpress/element';

interface ViewportDimensions {
width: number | null;
height: number | null;
}

/**
* Hook to track viewport dimensions.
*
* @since DOKAN_PRO_SINCE
*
* @return {ViewportDimensions} The viewport dimensions.
*/
export default function useWindowDimensions() {
const getViewportDimensions = useCallback((): ViewportDimensions => ({
width: typeof window !== 'undefined' ? window.innerWidth : null,
height: typeof window !== 'undefined' ? window.innerHeight : null,
}), []);

const [viewport, setViewport] = useState<ViewportDimensions>(getViewportDimensions());

useEffect(() => {
if (typeof window === 'undefined') {
return;
}

const handleResize = () => {
// Use requestAnimationFrame to throttle updates
window.requestAnimationFrame(() => {
setViewport(getViewportDimensions());
});
};

window.addEventListener('resize', handleResize);

// Initial measurement after mount
handleResize();

return () => {
window.removeEventListener('resize', handleResize);
};
}, [getViewportDimensions]);

return viewport;
};
Loading