diff --git a/.eslintrc.js b/.eslintrc.js index 35b2a77f..da964bb6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -159,7 +159,7 @@ module.exports = { "no-caller": "error", "no-cond-assign": "error", "no-console": "off", - "no-debugger": "error", + "no-debugger": "warn", "no-duplicate-case": "error", "no-duplicate-imports": "error", "no-empty": "error", diff --git a/package.json b/package.json index 41409b56..67e4e47a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ka-table", - "version": "8.6.0", + "version": "8.7.0", "license": "MIT", "repository": "github:komarovalexander/ka-table", "homepage": "https://komarovalexander.github.io/ka-table/#/overview", diff --git a/src/Demos/Demos.tsx b/src/Demos/Demos.tsx index dfb90915..a895be05 100644 --- a/src/Demos/Demos.tsx +++ b/src/Demos/Demos.tsx @@ -40,6 +40,7 @@ import GroupingCustomRowDemo from './GroupingCustomRowDemo/GroupingCustomRowDemo import GroupingDemo from './GroupingDemo/GroupingDemo'; import GroupingSummaryDemo from './GroupingSummaryDemo/GroupingSummaryDemo'; import HeaderFilterDemo from './HeaderFilterDemo/HeaderFilterDemo'; +import HeaderFilterLogicDemo from './HeaderFilterLogicDemo/HeaderFilterLogicDemo'; import HoverRowDemo from './HoverRowDemo/HoverRowDemo'; import InfiniteScrollingDemo from './InfiniteScrollingDemo/InfiniteScrollingDemo'; import InsertRowDemo from './InsertRowDemo/InsertRowDemo'; @@ -113,6 +114,7 @@ const demos: Demo[] = [ new Demo(GroupingCustomRowDemo, '/grouping-custom-row', 'Grouping Custom Row', 'GroupingCustomRowDemo', 'https://stackblitz.com/edit/table-grouping-custom-row-js', 'https://stackblitz.com/edit/table-grouping-custom-row-ts', 'Grouping'), new Demo(GroupingDemo, '/grouping', 'Grouping', 'GroupingDemo', 'https://stackblitz.com/edit/table-grouping-js', 'https://stackblitz.com/edit/table-grouping-ts', 'Grouping'), new Demo(HeaderFilterDemo, '/header-filter', 'Header Filter', 'HeaderFilterDemo', 'https://stackblitz.com/edit/table-header-filter-js', 'https://stackblitz.com/edit/table-header-filter-ts', 'Filtering'), + new Demo(HeaderFilterLogicDemo, '/header-filter-logic', 'Header Filter - Logic', 'HeaderFilterLogicDemo', 'https://stackblitz.com/edit/table-header-filter-logic-js', 'https://stackblitz.com/edit/table-header-filter-logic-ts', 'Filtering'), new Demo(GroupingSummaryDemo, '/grouping-summary', 'Grouping Summary', 'GroupingSummaryDemo', 'https://stackblitz.com/edit/table-grouping-summary-js', 'https://stackblitz.com/edit/table-grouping-summary-ts', 'Grouping'), new Demo(HoverRowDemo, '/hover-row', 'Hover Row', 'HoverRowDemo', 'https://stackblitz.com/edit/table-hover-row-js', 'https://stackblitz.com/edit/table-hover-row-ts', 'Rows'), new Demo(JsonDemo, '/json', 'Json', 'JsonDemo', 'https://stackblitz.com/edit/table-json-js', 'https://stackblitz.com/edit/table-json-ts', 'Remote Data'), diff --git a/src/Demos/FilterRowCustomLogicDemo/FilterRowCustomLogicDemo.tsx b/src/Demos/FilterRowCustomLogicDemo/FilterRowCustomLogicDemo.tsx index 64eff5be..73deb258 100644 --- a/src/Demos/FilterRowCustomLogicDemo/FilterRowCustomLogicDemo.tsx +++ b/src/Demos/FilterRowCustomLogicDemo/FilterRowCustomLogicDemo.tsx @@ -1,8 +1,8 @@ -import React from 'react'; - import { DataType, Table } from '../../lib'; + import FilterRowNumber from '../../lib/Components/FilterRowNumber/FilterRowNumber'; import { FilteringMode } from '../../lib/enums'; +import React from 'react'; const dataArray: any[] = [ { id: 1, name: 'Mike Wazowski', score: 80, prevScores: [60, 65, 70], passed: true, nextTry: new Date(2021, 10, 9) }, @@ -42,6 +42,7 @@ const FilterRowCustomLogicDemo: React.FC = () => { filterRowValue: 60, key: 'prevScores', style: {width: 120}, + filter: (value: number[], filterRowValue: number) => value.includes(Number(filterRowValue)), title: 'Previous Scores', } ]} @@ -51,11 +52,6 @@ const FilterRowCustomLogicDemo: React.FC = () => { return value.join(); } }} - filter= {({ column }) => { - if (column.key === 'prevScores') { - return (value: number[], filterRowValue: number) => value.includes(Number(filterRowValue)); - } - }} filteringMode={FilteringMode.FilterRow} rowKeyField={'id'} childComponents={{ diff --git a/src/Demos/HeaderFilterLogicDemo/HeaderFilterLogicDemo.test.tsx b/src/Demos/HeaderFilterLogicDemo/HeaderFilterLogicDemo.test.tsx new file mode 100644 index 00000000..53b90647 --- /dev/null +++ b/src/Demos/HeaderFilterLogicDemo/HeaderFilterLogicDemo.test.tsx @@ -0,0 +1,10 @@ +import HeaderFilterLogicDemo from './HeaderFilterLogicDemo'; +import React from 'react'; +import { createRoot } from 'react-dom/client'; + +it('renders without crashing', () => { + const div = document.createElement('div'); + const root = createRoot(div!); + root.render(); + root.unmount(); +}); diff --git a/src/Demos/HeaderFilterLogicDemo/HeaderFilterLogicDemo.tsx b/src/Demos/HeaderFilterLogicDemo/HeaderFilterLogicDemo.tsx new file mode 100644 index 00000000..65a6ef26 --- /dev/null +++ b/src/Demos/HeaderFilterLogicDemo/HeaderFilterLogicDemo.tsx @@ -0,0 +1,115 @@ +import { DataType, Table } from '../../lib'; +import { FilteringMode, SortDirection, SortingMode } from '../../lib/enums'; + +import React from 'react'; + +const dataArray: any[] = [ + { + id: 1, + name: 'Mike Wazowski', + score: 80, + passed: true, + nextTry: new Date(2021, 10, 8), + departments: [{ name: 'Department A', id: 1 }, { name: 'Department B', id: 2 }] + }, + { + id: 2, + name: 'Billi Bob', + score: 55, + passed: false, + nextTry: new Date(2021, 10, 8), + departments: [{ name: 'Department C', id: 3 }] + }, + { + id: 3, + name: 'Tom Williams', + score: 55, + passed: false, + nextTry: new Date(2021, 11, 8), + departments: [{ name: 'Department A', id: 1 }] + }, + { + id: 4, + name: 'Kurt Cobain', + score: 75, + passed: true, + nextTry: new Date(2021, 12, 9) + }, + { + id: 5, + name: 'Marshall Bruce', + score: 77, + passed: true, + nextTry: new Date(2021, 11, 12) + }, + { + id: 6, + name: 'Sunny Fox', + score: 33, + passed: false, + nextTry: new Date(2021, 10, 9) + }, +]; + +const HeaderFilterLogicDemo = () => { + return ( + { + return filterValues?.some(x => value?.some(v => v.id === x.id)); + }, + headerFilterListItems: ({ data }) => { + const departments: any[] | undefined = data?.reduce((acc, item) => [...acc, ...item.departments || []], []); + const departmentsUniqueByKey = departments?.filter((item: any, index) => { + return departments?.findIndex(i => i.id === item.id) === index; + }); + return departmentsUniqueByKey || []; + } + }, + ]} + data={dataArray} + sortingMode={SortingMode.Single} + filteringMode={FilteringMode.HeaderFilter} + format={({ column, value }) => { + if (column.dataType === DataType.Date) { + return value && value.toLocaleDateString('en', { month: '2-digit', day: '2-digit', year: 'numeric' }); + } + if (column.key === 'departments') { + return value?.map((d: any) => d.name).join(', '); + } + }} + childComponents={{ + popupContentItemText: { + content: ({ item }) => item.name + } + }} + rowKeyField={'id'} + /> + ); +}; + +export default HeaderFilterLogicDemo; diff --git a/src/Demos/MenuItems.tsx b/src/Demos/MenuItems.tsx index f22dfce0..b80fe74b 100644 --- a/src/Demos/MenuItems.tsx +++ b/src/Demos/MenuItems.tsx @@ -14,8 +14,8 @@ export class MenuItem { public isActive?: boolean; } -const newItems: string[] = []; -const updateItems: string[] = ['GroupingDemo']; +const newItems: string[] = ['HeaderFilterLogicDemo']; +const updateItems: string[] = []; const MenuItems: React.FC<{ items: MenuItem[] }> = ({ items }) => { diff --git a/src/lib/Components/PopupContent/PopupContent.tsx b/src/lib/Components/PopupContent/PopupContent.tsx index 4417a8ad..b003b0bf 100644 --- a/src/lib/Components/PopupContent/PopupContent.tsx +++ b/src/lib/Components/PopupContent/PopupContent.tsx @@ -14,23 +14,25 @@ const PopupContent: React.FC = (props) => { dispatch, format } = props; - - let headerFilterValues = data?.map((item) => { - const value = getValueByColumn(item, column); - - const formattedValue = - (format && format({ column, value, rowData: item })) - || value?.toString(); - return formattedValue; - }); - - headerFilterValues = Array.from(new Set(headerFilterValues)); + let headerFilterValues: any[] | undefined; + if (column?.headerFilterListItems){ + headerFilterValues = column?.headerFilterListItems({ data }); + } else { + headerFilterValues = data?.map((item) => { + const value = getValueByColumn(item, column); + + const formattedValue = + (format && format({ column, value, rowData: item })) + || value?.toString(); + return formattedValue; + }); + headerFilterValues = Array.from(new Set(headerFilterValues)); + } const { elementAttributes, content } = getElementCustomization({ className: `${defaultOptions.css.popupContent}` }, props, childComponents?.popupContent ); - return (
{content || diff --git a/src/lib/Components/PopupContentItem/PopupContentItem.tsx b/src/lib/Components/PopupContentItem/PopupContentItem.tsx index b00100b9..a5528f88 100644 --- a/src/lib/Components/PopupContentItem/PopupContentItem.tsx +++ b/src/lib/Components/PopupContentItem/PopupContentItem.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; -import { updateHeaderFilterValues } from '../../actionCreators'; -import defaultOptions from '../../defaultOptions'; import { IPopupContentItemProps } from '../../props'; +import PopupContentItemText from '../PopupContentItemText/PopupContentItemText'; +import defaultOptions from '../../defaultOptions'; import { getElementCustomization } from '../../Utils/ComponentUtils'; +import { updateHeaderFilterValues } from '../../actionCreators'; const PopupContentItem: React.FC = (props) => { const { @@ -35,9 +36,7 @@ const PopupContentItem: React.FC = (props) => { onChange={handleChange} />
-
- {item} -
+ )} diff --git a/src/lib/Components/PopupContentItemText/PopupContentItemText.test.tsx b/src/lib/Components/PopupContentItemText/PopupContentItemText.test.tsx new file mode 100644 index 00000000..3de2ba23 --- /dev/null +++ b/src/lib/Components/PopupContentItemText/PopupContentItemText.test.tsx @@ -0,0 +1,17 @@ +import PopupContentItemText from './PopupContentItemText'; +import React from 'react'; +import { createRoot } from 'react-dom/client'; + +const props: any = { + column: {key: 'field'}, + childComponents: {}, + dispatch: () => {}, + item: '' +}; + +it('renders without crashing', () => { + const div = document.createElement('div'); + const root = createRoot(div!); + root.render(); + root.unmount(); +}); diff --git a/src/lib/Components/PopupContentItemText/PopupContentItemText.tsx b/src/lib/Components/PopupContentItemText/PopupContentItemText.tsx new file mode 100644 index 00000000..88b43b9b --- /dev/null +++ b/src/lib/Components/PopupContentItemText/PopupContentItemText.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; + +import { IPopupContentItemProps } from '../../props'; +import { getElementCustomization } from '../../Utils/ComponentUtils'; + +const PopupContentItemText: React.FC = (props) => { + const { + childComponents, + item + } = props; + + const { elementAttributes, content } = getElementCustomization({ + className: 'ka-popup-content-item-value' + }, props, childComponents?.popupContentItemText); + + return ( +
+ {content || item.toString()} +
+ ) +} + +export default PopupContentItemText; diff --git a/src/lib/Models/ChildComponents.ts b/src/lib/Models/ChildComponents.ts index 496dd0ef..66d022ab 100644 --- a/src/lib/Models/ChildComponents.ts +++ b/src/lib/Models/ChildComponents.ts @@ -64,6 +64,7 @@ export class ChildComponents { public pagingSizes?: ChildComponent; public popupContent?: ChildComponent; public popupContentItem?: ChildComponent; + public popupContentItemText?: ChildComponent; public rootDiv?: ChildComponent; public sortIcon?: ChildComponent; public summaryCell?: ChildComponent; diff --git a/src/lib/Models/Column.ts b/src/lib/Models/Column.ts index 553c8c17..cf097fc8 100644 --- a/src/lib/Models/Column.ts +++ b/src/lib/Models/Column.ts @@ -11,6 +11,8 @@ export class Column { public filterRowValue?: any; public headerFilterValues?: any[]; public headerFilterPopupPosition?: PopupPosition; + public headerFilterListItems?: (props: { data?: any[] }) => any[]; + public filter?: (value: any, filterValue: any, rowData?: any) => boolean; public isHeaderFilterPopupShown?: boolean; public isEditable?: boolean; public isFilterable?: boolean; diff --git a/src/lib/Utils/FilterUtils.test.ts b/src/lib/Utils/FilterUtils.test.ts index cba390ba..5246cb2a 100644 --- a/src/lib/Utils/FilterUtils.test.ts +++ b/src/lib/Utils/FilterUtils.test.ts @@ -133,6 +133,17 @@ describe('FilterUtils', () => { }]; expect(() => filterData(data, columns)).toThrowError('\'unknownOperator\' has not found in predefinedFilterOperators array, available operators: =, >, <, >=, <=, contains'); }); + + it('custom column filter', () => { + const columns = [{ + dataType: DataType.Number, + filterRowValue: 45, + key: 'score', + filter: (value: any, filterValue: any) => value === filterValue, + }]; + const result = filterData(data, columns); + expect(result).toMatchSnapshot(); + }); }); [{ diff --git a/src/lib/Utils/FilterUtils.ts b/src/lib/Utils/FilterUtils.ts index 66cb5886..c53a62c8 100644 --- a/src/lib/Utils/FilterUtils.ts +++ b/src/lib/Utils/FilterUtils.ts @@ -59,7 +59,7 @@ export const filterAndSearchData = (props: ITableProps) => { const getCompare = (column: Column) => { const filterRowOperator = column.filterRowOperator - || getDefaultOperatorForType(column.dataType || defaultOptions.columnDataType); + || getDefaultOperatorForType(column.dataType || defaultOptions.columnDataType); const filterOperator = predefinedFilterOperators.find((fo) => filterRowOperator === fo.name); if (!filterOperator) { throw new Error(`'${column.filterRowOperator}' has not found in predefinedFilterOperators array, available operators: ${predefinedFilterOperators.map((o) => o.name).join(', ')}`); @@ -71,12 +71,12 @@ export const filterData = (data: any[], columns: Column[], filter?: FilterFunc): return columns.reduce((initialData, column) => { if ( isEmpty(column.filterRowValue) - && column.filterRowOperator !== FilterOperatorName.IsEmpty - && column.filterRowOperator !== FilterOperatorName.IsNotEmpty + && column.filterRowOperator !== FilterOperatorName.IsEmpty + && column.filterRowOperator !== FilterOperatorName.IsNotEmpty ) { return initialData; } - const customFilter = filter?.({ column }); + const customFilter = column.filter || filter?.({ column }); const compare = customFilter || getCompare(column); return initialData.filter((d: any) => { @@ -136,16 +136,19 @@ export const filterByHeaderFilter = (data: any[], columns: Column[], format?: Fo return columns.reduce((initialData, column) => { if ( isEmpty(column.headerFilterValues) - && column.filterRowOperator !== FilterOperatorName.IsEmpty - && column.filterRowOperator !== FilterOperatorName.IsNotEmpty + && column.filterRowOperator !== FilterOperatorName.IsEmpty + && column.filterRowOperator !== FilterOperatorName.IsNotEmpty ) { return initialData; } return initialData.filter((item: any) => { const value: any = getValueByColumn(item, column); + if (column?.filter) { + return column.filter(value, column.headerFilterValues, item); + } const fieldValue = - (format && format({ column, value, rowData: item })) - || value?.toString(); + (format && format({ column, value, rowData: item })) + || value?.toString(); return column.headerFilterValues?.includes(fieldValue); }); }, data); diff --git a/src/lib/Utils/__snapshots__/FilterUtils.test.ts.snap b/src/lib/Utils/__snapshots__/FilterUtils.test.ts.snap index 08b2872b..058f0a93 100644 --- a/src/lib/Utils/__snapshots__/FilterUtils.test.ts.snap +++ b/src/lib/Utils/__snapshots__/FilterUtils.test.ts.snap @@ -71,6 +71,18 @@ Array [ ] `; +exports[`FilterUtils filterData custom column filter 1`] = ` +Array [ + Object { + "date": 2021-11-20T13:00:00.000Z, + "id": 3, + "name": "Tom Williams", + "passed": false, + "score": 45, + }, +] +`; + exports[`FilterUtils filterData custom filter 1`] = ` Array [ Object {