|
| 1 | +import { |
| 2 | + InfiniteTable, |
| 3 | + DataSource, |
| 4 | + GroupRowsState, |
| 5 | + InfiniteTablePropColumnTypes, |
| 6 | + DataSourcePropRowSelection_MultiRow, |
| 7 | + InfiniteTableColumn, |
| 8 | + InfiniteTableColumnRenderValueParam, |
| 9 | + DataSourcePropAggregationReducers, |
| 10 | + DataSourceGroupBy, |
| 11 | + components, |
| 12 | + DataSourcePropFilterValue, |
| 13 | +} from '@infinite-table/infinite-react'; |
| 14 | +import * as React from 'react'; |
| 15 | +import { useState } from 'react'; |
| 16 | + |
| 17 | +const { CheckBox } = components; |
| 18 | + |
| 19 | +type Developer = { |
| 20 | + id: number; |
| 21 | + firstName: string; |
| 22 | + lastName: string; |
| 23 | + country: string; |
| 24 | + city: string; |
| 25 | + currency: string; |
| 26 | + preferredLanguage: string; |
| 27 | + stack: string; |
| 28 | + canDesign: 'yes' | 'no'; |
| 29 | + hobby: string; |
| 30 | + salary: number; |
| 31 | + age: number; |
| 32 | +}; |
| 33 | + |
| 34 | +const avgReducer = { |
| 35 | + initialValue: 0, |
| 36 | + reducer: (acc: number, sum: number) => acc + sum, |
| 37 | + done: (value: number, arr: any[]) => |
| 38 | + arr.length ? Math.floor(value / arr.length) : 0, |
| 39 | +}; |
| 40 | +const aggregationReducers: DataSourcePropAggregationReducers<Developer> = { |
| 41 | + salary: { |
| 42 | + field: 'salary', |
| 43 | + |
| 44 | + ...avgReducer, |
| 45 | + }, |
| 46 | + age: { |
| 47 | + field: 'age', |
| 48 | + ...avgReducer, |
| 49 | + }, |
| 50 | + currency: { |
| 51 | + field: 'currency', |
| 52 | + initialValue: new Set<string>(), |
| 53 | + reducer: (acc: Set<string>, value: string) => { |
| 54 | + acc.add(value); |
| 55 | + return acc; |
| 56 | + }, |
| 57 | + done: (value: Set<string>) => { |
| 58 | + return value.size > 1 ? 'Mixed' : value.values().next().value; |
| 59 | + }, |
| 60 | + }, |
| 61 | + canDesign: { |
| 62 | + field: 'canDesign', |
| 63 | + |
| 64 | + initialValue: false, |
| 65 | + reducer: (acc: boolean | null, value: 'yes' | 'no') => { |
| 66 | + if (acc === null) { |
| 67 | + return acc; |
| 68 | + } |
| 69 | + if (acc === false && value === 'yes') { |
| 70 | + return null; |
| 71 | + } |
| 72 | + if (acc === true && value === 'no') { |
| 73 | + return null; |
| 74 | + } |
| 75 | + return acc; |
| 76 | + }, |
| 77 | + }, |
| 78 | +}; |
| 79 | + |
| 80 | +const flags = { |
| 81 | + 'United States': '🇺🇸', |
| 82 | + Canada: '🇨🇦', |
| 83 | + France: '🇫🇷', |
| 84 | + Germany: '🇩🇪', |
| 85 | + 'United Kingdom': '🇬🇧', |
| 86 | + 'South Africa': '🇿🇦', |
| 87 | + 'New Zealand': '🇳🇿', |
| 88 | + Sweden: '🇸🇪', |
| 89 | + China: '🇨🇳', |
| 90 | + Brazil: '🇧🇷', |
| 91 | + Turkey: '🇹🇷', |
| 92 | + Italy: '🇮🇹', |
| 93 | + India: '🇮🇳', |
| 94 | + Indonesia: '🇮🇩', |
| 95 | + Japan: '🇯🇵', |
| 96 | + Argentina: '🇦🇷', |
| 97 | + 'Saudi Arabia': '🇸🇦', |
| 98 | + Mexico: '🇲🇽', |
| 99 | + 'United Arab Emirates': '🇦🇪', |
| 100 | +}; |
| 101 | +function getColumns(): Record<string, InfiniteTableColumn<Developer>> { |
| 102 | + return { |
| 103 | + age: { |
| 104 | + field: 'age', |
| 105 | + header: 'Age', |
| 106 | + type: 'number', |
| 107 | + defaultWidth: 100, |
| 108 | + renderValue: ({ value }) => value, |
| 109 | + }, |
| 110 | + salary: { |
| 111 | + header: 'Compensation', |
| 112 | + field: 'salary', |
| 113 | + type: 'number', |
| 114 | + defaultWidth: 210, |
| 115 | + }, |
| 116 | + currency: { field: 'currency', header: 'Currency', defaultWidth: 100 }, |
| 117 | + preferredLanguage: { |
| 118 | + field: 'preferredLanguage', |
| 119 | + header: 'Programming Language', |
| 120 | + }, |
| 121 | + |
| 122 | + canDesign: { |
| 123 | + defaultWidth: 135, |
| 124 | + field: 'canDesign', |
| 125 | + header: 'Design Skills', |
| 126 | + renderMenuIcon: false, |
| 127 | + renderValue: ({ value }) => { |
| 128 | + return ( |
| 129 | + <div style={{ display: 'flex', alignItems: 'center' }}> |
| 130 | + <CheckBox |
| 131 | + defaultChecked={value === null ? null : value === 'yes'} |
| 132 | + domProps={{ |
| 133 | + style: { |
| 134 | + marginRight: 10, |
| 135 | + }, |
| 136 | + }} |
| 137 | + /> |
| 138 | + {value === null ? 'Some' : value === 'yes' ? 'Yes' : 'No'} |
| 139 | + </div> |
| 140 | + ); |
| 141 | + }, |
| 142 | + }, |
| 143 | + country: { |
| 144 | + field: 'country', |
| 145 | + header: 'Country', |
| 146 | + renderValue: ({ value }) => { |
| 147 | + return ( |
| 148 | + <span> |
| 149 | + {(flags as any)[value] || null} {value} |
| 150 | + </span> |
| 151 | + ); |
| 152 | + }, |
| 153 | + }, |
| 154 | + firstName: { field: 'firstName', header: 'First Name' }, |
| 155 | + stack: { field: 'stack', header: 'Stack' }, |
| 156 | + |
| 157 | + city: { |
| 158 | + field: 'city', |
| 159 | + header: 'City', |
| 160 | + renderHeader: ({ column }) => `${column.computedVisibleIndex} City`, |
| 161 | + }, |
| 162 | + }; |
| 163 | +} |
| 164 | + |
| 165 | +// 250+100+210+100+150+135+150+150+150+150 |
| 166 | +// → 123.456,789 |
| 167 | +const groupColumn: InfiniteTableColumn<Developer> = { |
| 168 | + header: 'Grouping', |
| 169 | + field: 'firstName', |
| 170 | + defaultWidth: 250, |
| 171 | + renderSelectionCheckBox: true, |
| 172 | + defaultFilterable: false, |
| 173 | + |
| 174 | + // in this function we have access to collapsed info |
| 175 | + // and grouping info about the current row - see rowInfo.groupBy |
| 176 | + renderValue: ({ |
| 177 | + value, |
| 178 | + rowInfo, |
| 179 | + }: InfiniteTableColumnRenderValueParam<Developer>) => { |
| 180 | + if (!rowInfo.isGroupRow) { |
| 181 | + return `${rowInfo.indexInAll} ${rowInfo.id} ${value}`; |
| 182 | + } |
| 183 | + const groupBy = rowInfo.groupBy || []; |
| 184 | + const collapsed = rowInfo.collapsed; |
| 185 | + const currentGroupBy = groupBy[groupBy.length - 1]; |
| 186 | + |
| 187 | + if (currentGroupBy?.field === 'age') { |
| 188 | + return `🥳 ${value}${collapsed ? ' 🤷♂️' : ''}`; |
| 189 | + } |
| 190 | + |
| 191 | + return `${rowInfo.indexInAll} ${value}`; |
| 192 | + }, |
| 193 | +}; |
| 194 | + |
| 195 | +const defaultGroupRowsState = new GroupRowsState({ |
| 196 | + //make all groups collapsed by default |
| 197 | + collapsedRows: true, |
| 198 | + expandedRows: [ |
| 199 | + ['United States'], |
| 200 | + ['United States', 'backend'], |
| 201 | + ['France'], |
| 202 | + ['Turkey'], |
| 203 | + ], |
| 204 | +}); |
| 205 | + |
| 206 | +const columnTypes: InfiniteTablePropColumnTypes<Developer> = { |
| 207 | + number: { |
| 208 | + align: 'end', |
| 209 | + style: () => { |
| 210 | + return {}; |
| 211 | + }, |
| 212 | + renderValue: ({ value, data, rowInfo }) => { |
| 213 | + return new Intl.NumberFormat('en-US', { |
| 214 | + style: 'currency', |
| 215 | + currency: |
| 216 | + rowInfo.isGroupRow && rowInfo.data?.currency === 'Mixed' |
| 217 | + ? 'USD' |
| 218 | + : data?.currency || 'USD', |
| 219 | + }).format(value); |
| 220 | + }, |
| 221 | + }, |
| 222 | +}; |
| 223 | + |
| 224 | +const defaultRowSelection: DataSourcePropRowSelection_MultiRow = { |
| 225 | + selectedRows: [['United States'], ['India'], ['France'], ['Turkey']], |
| 226 | + deselectedRows: [['United States', 'frontend']], |
| 227 | + defaultSelection: false, |
| 228 | +}; |
| 229 | + |
| 230 | +const defaultFilterValue: DataSourcePropFilterValue<Developer> = [ |
| 231 | + { |
| 232 | + field: 'age', |
| 233 | + filter: { |
| 234 | + operator: 'gt', |
| 235 | + type: 'number', |
| 236 | + value: null, |
| 237 | + }, |
| 238 | + }, |
| 239 | +]; |
| 240 | + |
| 241 | +const domProps = { |
| 242 | + style: { |
| 243 | + // minHeight: '50vh', |
| 244 | + width: '90vw', |
| 245 | + height: '90vh', |
| 246 | + margin: 5, |
| 247 | + }, |
| 248 | +}; |
| 249 | + |
| 250 | +export default function App() { |
| 251 | + const [{ min, max }, setMinMax] = useState({ min: 0, max: 0 }); |
| 252 | + const columns = React.useMemo(() => { |
| 253 | + const cols = getColumns(); |
| 254 | + |
| 255 | + if (cols.salary) { |
| 256 | + cols.salary.render = ({ renderBag, value }) => { |
| 257 | + const increase: number = Math.abs(max - min); |
| 258 | + const percentage = ((value - min) / increase) * 100; |
| 259 | + const alpha = Number((percentage / 100).toPrecision(2)) + 0.2; |
| 260 | + |
| 261 | + const backgroundColor = `rgba(255, 0, 0, ${alpha})`; |
| 262 | + return ( |
| 263 | + <div |
| 264 | + style={{ |
| 265 | + position: 'absolute', |
| 266 | + display: 'flex', |
| 267 | + alignItems: 'center', |
| 268 | + justifyContent: 'flex-end', |
| 269 | + top: 0, |
| 270 | + bottom: 0, |
| 271 | + left: 0, |
| 272 | + right: 0, |
| 273 | + backgroundColor, |
| 274 | + }} |
| 275 | + > |
| 276 | + {renderBag.all} |
| 277 | + </div> |
| 278 | + ); |
| 279 | + }; |
| 280 | + } |
| 281 | + |
| 282 | + return cols; |
| 283 | + }, [min, max]); |
| 284 | + |
| 285 | + const groupBy: DataSourceGroupBy<Developer>[] = React.useMemo( |
| 286 | + () => [ |
| 287 | + { |
| 288 | + field: 'country', |
| 289 | + }, |
| 290 | + { field: 'stack' }, |
| 291 | + ], |
| 292 | + [], |
| 293 | + ); |
| 294 | + |
| 295 | + const [columnVisibility, setColumnVisibility] = useState< |
| 296 | + Record<string, false> |
| 297 | + >({}); |
| 298 | + return ( |
| 299 | + <> |
| 300 | + <button |
| 301 | + onClick={() => |
| 302 | + setColumnVisibility({ |
| 303 | + age: false, |
| 304 | + salary: false, |
| 305 | + currency: false, |
| 306 | + preferredLanguage: false, |
| 307 | + |
| 308 | + canDesign: false, |
| 309 | + country: false, |
| 310 | + firstName: false, |
| 311 | + stack: false, |
| 312 | + |
| 313 | + city: false, |
| 314 | + 'group-by': false, |
| 315 | + }) |
| 316 | + } |
| 317 | + > |
| 318 | + hide all |
| 319 | + </button> |
| 320 | + <DataSource<Developer> |
| 321 | + data={dataSource} |
| 322 | + primaryKey="id" |
| 323 | + defaultFilterValue={defaultFilterValue} |
| 324 | + filterMode="local" |
| 325 | + useGroupKeysForMultiRowSelection |
| 326 | + defaultRowSelection={defaultRowSelection} |
| 327 | + defaultSortInfo={{ |
| 328 | + dir: -1, |
| 329 | + field: 'country', |
| 330 | + }} |
| 331 | + onDataArrayChange={(data) => { |
| 332 | + const min = Math.min(...data.map((data) => data.salary ?? 0)); |
| 333 | + const max = Math.max(...data.map((data) => data.salary ?? 0)); |
| 334 | + |
| 335 | + setMinMax({ min, max }); |
| 336 | + }} |
| 337 | + defaultGroupRowsState={defaultGroupRowsState} |
| 338 | + aggregationReducers={aggregationReducers} |
| 339 | + groupBy={groupBy} |
| 340 | + > |
| 341 | + <InfiniteTable<Developer> |
| 342 | + debugId="full-demo" |
| 343 | + groupRenderStrategy="single-column" |
| 344 | + defaultColumnPinning={{ |
| 345 | + 'group-by': true, |
| 346 | + }} |
| 347 | + columnVisibility={columnVisibility} |
| 348 | + domProps={domProps} |
| 349 | + defaultActiveRowIndex={0} |
| 350 | + groupColumn={groupColumn} |
| 351 | + licenseKey={process.env.NEXT_PUBLIC_INFINITE_LICENSE_KEY} |
| 352 | + columns={columns} |
| 353 | + columnTypes={columnTypes} |
| 354 | + columnDefaultWidth={150} |
| 355 | + /> |
| 356 | + </DataSource> |
| 357 | + </> |
| 358 | + ); |
| 359 | +} |
| 360 | + |
| 361 | +const dataSource = () => { |
| 362 | + return fetch(process.env.NEXT_PUBLIC_BASE_URL + '/developers100') |
| 363 | + .then((r) => r.json()) |
| 364 | + .then((data: Developer[]) => data); |
| 365 | +}; |
0 commit comments