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 23f63b2..e05b812 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "version": "0.13.2",
+ "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/grid.tsx b/src/components/grid.tsx
index 133bb43..d2fcbb9 100644
--- a/src/components/grid.tsx
+++ b/src/components/grid.tsx
@@ -527,7 +527,7 @@ export function Grid(props: GridProps) {
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 67d1b0c..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)
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"