diff --git a/README.md b/README.md index f12af83..f967a86 100644 --- a/README.md +++ b/README.md @@ -88,9 +88,9 @@ The user can interact with the table and update the filters, but the table will ### defaultSort -`string` +`array` -The name of the column you want the table to initialize sorting by. The user can interact with the table and update the sort, but the table will use the default sort when `defaultSort` or `data` changes. +The name of the column and the order you want the table to initialize sorting by (e.g. `["Location", "desc"]`). The user can interact with the table and update the sort, but the table will use the default sort when `defaultSort` or `data` changes. ### defaultStickyColumnName diff --git a/package.json b/package.json index 78cf4e2..e05b812 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.13.0", + "version": "0.14.1", "license": "MIT", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -72,6 +72,7 @@ "@storybook/addons": "^6.1.21", "@storybook/react": "^6.1.21", "@types/d3": "^6.3.0", + "@types/linkify-it": "^3.0.2", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.2", "@types/react-virtualized": "^9.21.11", @@ -101,12 +102,12 @@ "@types/lodash": "^4.0.6", "@types/react-virtualized-auto-sizer": "^1.0.0", "@types/react-window": "^1.8.2", - "anchorme": "^2.1.2", "d3": "^6.6.0", "date-fns": "^2.19.0", "dompurify": "^2.2.9", "downshift": "^6.1.1", - "immer": "^8.0.2", + "immer": "^9.0.12", + "linkify-it": "^3.0.3", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "match-sorter": "^6.3.0", @@ -115,6 +116,6 @@ "react-virtualized-auto-sizer": "^1.0.5", "react-window": "^1.8.6", "twin.macro": "^2.6.2", - "zustand": "^3.3.3" + "zustand": "^3.6.9" } } diff --git a/src/components/cell.tsx b/src/components/cell.tsx index 8eba132..8276273 100644 --- a/src/components/cell.tsx +++ b/src/components/cell.tsx @@ -1,12 +1,14 @@ import React, { useEffect } from 'react'; import { areEqual } from 'react-window'; import tw, { TwStyle } from 'twin.macro'; -import anchorme from 'anchorme'; +import Linkify from 'linkify-it'; import { cellTypeMap } from '../store'; import { DashIcon, DiffModifiedIcon, PlusIcon } from '@primer/octicons-react'; import DOMPurify from 'dompurify'; import { EditableCell } from './editable-cell'; +const linkify = Linkify().add('ftp:', null).add('mailto:', null); + interface CellProps { type: string; value: any; @@ -47,37 +49,39 @@ export const Cell = React.memo(function (props: CellProps) { onFocusChange, background, style = {}, - onMouseEnter = () => { }, + onMouseEnter = () => {}, } = props; // @ts-ignore const cellInfo = cellTypeMap[type]; - const { cell: CellComponent } = cellInfo || {} + const { cell: CellComponent } = cellInfo || {}; - const displayValue = (formattedValue || value || "").toString(); + const displayValue = (formattedValue || value || '').toString(); const isLongValue = (displayValue || '').length > 23; - const stringWithLinks = React.useMemo( - () => displayValue ? ( - DOMPurify.sanitize( - anchorme({ - input: displayValue + '', - options: { - attributes: { - target: '_blank', - rel: 'noopener', - }, - }, - }) - ) - ) : "", - [value] - ) + const stringWithLinks = React.useMemo(() => { + if (!displayValue) return ''; + + const sanitized = DOMPurify.sanitize(displayValue); + // Does the sanitized string contain any links? + if (!linkify.test(sanitized)) return sanitized; + + // If so, we need to linkify it. + const matches = linkify.match(sanitized); + + // If there are no matches, we can just return the sanitized string. + if (!matches || matches.length === 0) return sanitized; + + // Otherwise, let's naively use the first match. + return ` + ${matches[0].url} + `; + }, [value]); useEffect(() => { - if (!isFocused) return - onMouseEnter() - }, [isFocused]) + if (!isFocused) return; + onMouseEnter(); + }, [isFocused]); if (!cellInfo) return null; @@ -91,14 +95,15 @@ export const Cell = React.memo(function (props: CellProps) { 'modified-row': DiffModifiedIcon, }[status || '']; const statusColor = - isFirstColumn && - // @ts-ignore - { - new: 'text-green-400', - old: 'text-pink-400', - modified: 'text-yellow-500', - 'modified-row': 'text-yellow-500', - }[status || ''] || "" + (isFirstColumn && + // @ts-ignore + { + new: 'text-green-400', + old: 'text-pink-400', + modified: 'text-yellow-500', + 'modified-row': 'text-yellow-500', + }[status || '']) || + ''; return (
+ }} + > + onRowDelete={onRowDelete} + > onMouseEnter?.()} > @@ -214,5 +220,5 @@ const CellInner = React.memo(function CellInner({
)} - ) -}) \ No newline at end of file + ); +}); diff --git a/src/components/cells/color.tsx b/src/components/cells/color.tsx index d0b2f33..3dbd791 100644 --- a/src/components/cells/color.tsx +++ b/src/components/cells/color.tsx @@ -11,7 +11,7 @@ interface ColorCellProps { export function ColorCell(props: ColorCellProps) { return ( -
Showing {filteredData.length.toLocaleString()} {isFiltered && ` of ${data.length.toLocaleString()}`} row - {(isFiltered ? filteredData : data).length === 1 ? '' : 's'} + {data.length === 1 ? '' : 's'} × {columnNames.length.toLocaleString()} column{columnNames.length === 1 ? '' : 's'}
diff --git a/src/components/sticky-grid.tsx b/src/components/sticky-grid.tsx index 06d7d46..dfcce35 100644 --- a/src/components/sticky-grid.tsx +++ b/src/components/sticky-grid.tsx @@ -6,7 +6,10 @@ import tw from 'twin.macro'; import { FilterValue } from '../types'; function getCellIndicies(child) { - return { row: child.props.rowIndex, column: child.props.columnIndex }; + return { + row: child?.props.rowIndex || 0, + column: child?.props.columnIndex || 0 + }; } function getShownIndicies(children) { diff --git a/src/store.ts b/src/store.ts index 3daafd7..070febd 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,6 +1,13 @@ import tw from 'twin.macro'; -import create, { StateCreator } from 'zustand'; +import create, { + GetState, + SetState, + State, + StateCreator, + StoreApi, +} from 'zustand'; import produce from 'immer'; +import type { Draft } from 'immer'; import { format as d3Format, timeFormat, @@ -29,10 +36,34 @@ import { StringFilter } from './components/filters/string'; import { CategoryFilter } from './components/filters/category'; import { RangeFilter } from './components/filters/range'; -export const immer = ( - config: StateCreator void) => void> -): StateCreator => (set, get, api) => - config((fn) => set(produce(fn) as (state: T) => T), get, api); +const immer = < + T extends State, + CustomSetState extends SetState, + CustomGetState extends GetState, + CustomStoreApi extends StoreApi +>( + config: StateCreator< + T, + (partial: ((draft: Draft) => void) | T, replace?: boolean) => void, + CustomGetState, + CustomStoreApi + > +): StateCreator => ( + set, + get, + api +) => + config( + (partial, replace) => { + const nextState = + typeof partial === 'function' + ? produce(partial as (state: Draft) => T) + : (partial as T); + return set(nextState, replace); + }, + get, + api + ); export type GridState = { data: any[]; @@ -513,6 +544,10 @@ const getSortFunction = (sort: string[], typeOfValue: string) => { aVal = (aVal || '')?.toUpperCase?.() || ''; if (!aVal || aVal === '\n') aVal = direction === 'asc' ? 'zzzzzz' : ''; aVal = aVal.trimStart(); + } else if (typeOfValue === 'number') { + aVal = Number.isFinite(aVal) + ? aVal + : Infinity * (direction === 'asc' ? 1 : -1); } // @ts-ignore let bVal = b[columnName]; @@ -520,8 +555,11 @@ const getSortFunction = (sort: string[], typeOfValue: string) => { bVal = (bVal || '')?.toUpperCase?.() || ''; if (!bVal || bVal === '\n') bVal = direction === 'asc' ? 'zzzzzz' : ''; bVal = bVal.trimStart(); + } else if (typeOfValue === 'number') { + bVal = Number.isFinite(bVal) + ? bVal + : Infinity * (direction === 'asc' ? 1 : -1); } - return direction == 'desc' ? // @ts-ignore descending(aVal, bVal) @@ -577,7 +615,7 @@ function generateSchema(data: any[]) { try { if (typeof value === 'string') { const color = rgb(value); - return !!color; + return !!color && !Number.isNaN(color.r); } return false; } catch (e) { diff --git a/yarn.lock b/yarn.lock index 4a33c5a..f88d809 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3251,6 +3251,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.8.tgz#edf1bf1dbf4e04413ca8e5b17b3b7d7d54b59818" integrity sha512-YSBPTLTVm2e2OoQIDYx8HaeWJ5tTToLH67kXR7zYNGupXMEHa2++G8k+DczX2cFVgalypqtyZIcU19AFcmOpmg== +"@types/linkify-it@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" + integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== + "@types/lodash@^4.0.6": version "4.14.171" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.171.tgz#f01b3a5fe3499e34b622c362a46a609fdb23573b" @@ -3837,11 +3842,6 @@ alphanum-sort@^1.0.0, alphanum-sort@^1.0.2: resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= -anchorme@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/anchorme/-/anchorme-2.1.2.tgz#4abc7e128a8a42d0036a61ebb9b18bbc032fa52a" - integrity sha512-2iPY3kxDDZvtRzauqKDb4v7a5sTF4GZ+esQTY8nGYvmhAtGTeFPMn4cRnvyWS1qmtPTP0Mv8hyLOp9l3ZzWMKg== - ansi-align@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" @@ -8398,10 +8398,10 @@ immer@8.0.1: resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== -immer@^8.0.2: - version "8.0.4" - resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.4.tgz#3a21605a4e2dded852fb2afd208ad50969737b7a" - integrity sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ== +immer@^9.0.12: + version "9.0.12" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20" + integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA== import-cwd@^3.0.0: version "3.0.0" @@ -9793,6 +9793,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= +linkify-it@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" + integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== + dependencies: + uc.micro "^1.0.1" + load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -14795,6 +14802,11 @@ ua-parser-js@^0.7.18: resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== +uc.micro@^1.0.1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" @@ -15567,10 +15579,10 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== -zustand@^3.3.3: - version "3.5.6" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.5.6.tgz#c28cfbdfdd999d26d1a94ea105a6fd1da56ed38a" - integrity sha512-8XrpRO5scF8MSxeAlu7vFupmLG+5MTWhT+6+3QNsihs0QZfOjaArFyvenUgrk30WdZVGVHLHXBhbqC2/QzLeMA== +zustand@^3.6.9: + version "3.6.9" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.6.9.tgz#f61a756ddea9f95c7ee7cfd3af2f88c10078afbc" + integrity sha512-OvDNu/jEWpRnEC7k8xh8GKjqYog7td6FZrLMuHs/IeI8WhrCwV+FngVuwMIFhp5kysZXr6emaeReMqjLGaldAQ== zwitch@^1.0.0: version "1.0.5"