diff --git a/README.md b/README.md index b691c83f..a9599654 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Features include: - fine-grained control over which elements can be edited, deleted, or added to - customisable UI, through simple, pre-defined [themes](#themes--styles), specific CSS overrides for UI components, or by targeting CSS classes - self-contained — rendered with plain HTML/CSS, so no dependance on external UI libraries + - search/filter data by key, value or custom function - provide your own [custom component](#custom-nodes) to integrate specialised UI for certain data. **[Explore the Demo](https://carlosnz.github.io/json-edit-react/)** @@ -24,6 +25,7 @@ Features include: - [Copy function](#copy-function) - [Filter functions](#filter-functions) - [Examples](#examples) +- [Search/Filtering](#searchfiltering) - [Themes \& Styles](#themes--styles) - [Fragments](#fragments) - [A note about sizing and scaling](#a-note-about-sizing-and-scaling) @@ -82,34 +84,37 @@ It's pretty self explanatory (click the "edit" icon to edit, etc.), but there ar The only *required* value is `data`. -| prop | type | default | description | -| ----------------------- | -------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `data` | `object\|array` | | The data to be displayed / edited | -| `rootName` | `string` | `"data"` | A name to display in the editor as the root of the data object. | -| `onUpdate` | `UpdateFunction` | | A function to run whenever a value is **updated** (edit, delete *or* add) in the editor. See [Update functions](#update-functions). | -| `onEdit` | `UpdateFunction` | | A function to run whenever a value is **edited**. | -| `onDelete` | `UpdateFunction` | | A function to run whenever a value is **deleted**. | -| `onAdd` | `UpdateFunction` | | A function to run whenever a new property is **added**. | -| `enableClipboard` | `boolean\|CopyFunction` | `true` | Whether or not to enable the "Copy to clipboard" button in the UI. If a function is provided, `true` is assumed and this function will be run whenever an item is copied. | -| `indent` | `number` | `3` | Specify the amount of indentation for each level of nesting in the displayed data. | -| `collapse` | `boolean\|number\|FilterFunction` | `false` | Defines which nodes of the JSON tree will be displayed "opened" in the UI on load. If `boolean`, it'll be either all or none. A `number` specifies a nesting depth after which nodes will be closed. For more fine control a function can be provided — see [Filter functions](#filter-functions). | -| `restrictEdit` | `boolean\|FilterFunction` | `false` | If `false`, no editing is permitted. A function can be provided for more specificity — see [Filter functions](#filter-functions) | -| `restrictDelete` | `boolean\|FilterFunction` | `false` | As with `restrictEdit` but for deletion | -| `restrictAdd` | `boolean\|FilterFunction` | `false` | As with `restrictEdit` but for adding new properties | -| `restrictTypeSelection` | `boolean\|DataType[]\|TypeFilterFunction` | `false` | For restricting the data types the user can select. Can be a list of data types (e.g. `[ 'string', 'number', 'boolean', 'array', 'object', 'null' ]`) or a boolean. A function can be provided -- it should take the same input as the above `FilterFunction`s, but output should be `boolean \| DataType[]`. | -| `keySort` | `boolean\|CompareFunction` | `false` | If `true`, object keys will be ordered (using default JS `.sort()`). A [compare function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) can also be provided to define sorting behaviour. | -| `showArrayIndices` | `boolean` | `true` | Whether or not to display the index (as a property key) for array elements. | -| `showCollectionCount` | `boolean\|"when-closed"` | `true` | Whether or not to display the number of items in each collection (object or array). | -| `defaultValue` | `any\|DefaultValueFilterFunction` | `null` | When a new property is added, it is initialised with this value. A function can be provided with the same input as the `FilterFunction`s, but should output a value. This allows a different default value to be used depending on the data state (e.g. default for top level is an object, but a string elsewhere.) | -| `stringTruncate` | `number` | `250` | String values longer than this many characters will be displayed truncated (with `...`). The full string will always be visible when editing. | -| `translations` | `LocalisedStrings` object | `{ }` | UI strings (such as error messages) can be translated by passing an object containing localised string values (there are only a few). See [Localisation](#localisation) | -| `theme` | `string\|ThemeObject\|[string, ThemeObject]` | `"default"` | Either the name of one of the built-in themes, or an object specifying some or all theme properties. See [Themes](#themes--styles). | -| `className` | `string` | | Name of a CSS class to apply to the component. In most cases, specifying `theme` properties will be more straightforward. | -| `icons` | `{[iconName]: JSX.Element, ... }` | `{ }` | Replace the built-in icons by specifying them here. See [Themes](#themes--styles). | | -| `minWidth` | `number\|string` (CSS value) | `250` | Minimum width for the editor container. | -| `maxWidth` | `number\|string` (CSS value) | `600` | Maximum width for the editor container. | -| `customNodeDefinitions` | `CustomNodeDefinition[]` | | You can provide customised components to override specific nodes in the data tree, according to a condition function. See see [Custom nodes](#custom-nodes) for more detail. (A simple custom component to turn url strings into active links is provided in the main package -- see [here](#active-hyperlinks)) | -| `customText` | `CustomTextDefinitions` | | In addition to [localising the component](#localisation) text strings, you can also *dynamically* alter it, depending on the data. See [Custom Text](#custom-text) for more detail. | +| prop | type | default | description | +| ----------------------- | --------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `data` | `object\|array` | | The data to be displayed / edited | +| `rootName` | `string` | `"data"` | A name to display in the editor as the root of the data object. | +| `onUpdate` | `UpdateFunction` | | A function to run whenever a value is **updated** (edit, delete *or* add) in the editor. See [Update functions](#update-functions). | +| `onEdit` | `UpdateFunction` | | A function to run whenever a value is **edited**. | +| `onDelete` | `UpdateFunction` | | A function to run whenever a value is **deleted**. | +| `onAdd` | `UpdateFunction` | | A function to run whenever a new property is **added**. | +| `enableClipboard` | `boolean\|CopyFunction` | `true` | Whether or not to enable the "Copy to clipboard" button in the UI. If a function is provided, `true` is assumed and this function will be run whenever an item is copied. | +| `indent` | `number` | `3` | Specify the amount of indentation for each level of nesting in the displayed data. | +| `collapse` | `boolean\|number\|FilterFunction` | `false` | Defines which nodes of the JSON tree will be displayed "opened" in the UI on load. If `boolean`, it'll be either all or none. A `number` specifies a nesting depth after which nodes will be closed. For more fine control a function can be provided — see [Filter functions](#filter-functions). | +| `restrictEdit` | `boolean\|FilterFunction` | `false` | If `false`, no editing is permitted. A function can be provided for more specificity — see [Filter functions](#filter-functions) | +| `restrictDelete` | `boolean\|FilterFunction` | `false` | As with `restrictEdit` but for deletion | +| `restrictAdd` | `boolean\|FilterFunction` | `false` | As with `restrictEdit` but for adding new properties | +| `restrictTypeSelection` | `boolean\|DataType[]\|TypeFilterFunction` | `false` | For restricting the data types the user can select. Can be a list of data types (e.g. `[ 'string', 'number', 'boolean', 'array', 'object', 'null' ]`) or a boolean. A function can be provided -- it should take the same input as the above `FilterFunction`s, but output should be `boolean \| DataType[]`. | +| `searchText` | `string` | `undefined` | Data visibility will be filtered by matching against value, using the method defined below in `searchFilter` | +| `searchFilter` | `"key"\|"value"\|"all"\|SearchFilterFunction` | `undefined` | Define how `searchText` should be matched to filter the visible items. See [Search/Filtering](#searchfiltering) | +| `searchDebounceTime` | `number` | `350` | Debounce time when `searchText` changes | +| `keySort` | `boolean\|CompareFunction` | `false` | If `true`, object keys will be ordered (using default JS `.sort()`). A [compare function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) can also be provided to define sorting behaviour. | +| `showArrayIndices` | `boolean` | `true` | Whether or not to display the index (as a property key) for array elements. | +| `showCollectionCount` | `boolean\|"when-closed"` | `true` | Whether or not to display the number of items in each collection (object or array). | +| `defaultValue` | `any\|DefaultValueFilterFunction` | `null` | When a new property is added, it is initialised with this value. A function can be provided with the same input as the `FilterFunction`s, but should output a value. This allows a different default value to be used depending on the data state (e.g. default for top level is an object, but a string elsewhere.) | +| `stringTruncate` | `number` | `250` | String values longer than this many characters will be displayed truncated (with `...`). The full string will always be visible when editing. | +| `translations` | `LocalisedStrings` object | `{ }` | UI strings (such as error messages) can be translated by passing an object containing localised string values (there are only a few). See [Localisation](#localisation) | +| `theme` | `string\|ThemeObject\|[string, ThemeObject]` | `"default"` | Either the name of one of the built-in themes, or an object specifying some or all theme properties. See [Themes](#themes--styles). | +| `className` | `string` | | Name of a CSS class to apply to the component. In most cases, specifying `theme` properties will be more straightforward. | +| `icons` | `{[iconName]: JSX.Element, ... }` | `{ }` | Replace the built-in icons by specifying them here. See [Themes](#themes--styles). | | +| `minWidth` | `number\|string` (CSS value) | `250` | Minimum width for the editor container. | +| `maxWidth` | `number\|string` (CSS value) | `600` | Maximum width for the editor container. | +| `customNodeDefinitions` | `CustomNodeDefinition[]` | | You can provide customised components to override specific nodes in the data tree, according to a condition function. See see [Custom nodes](#custom-nodes) for more detail. (A simple custom component to turn url strings into active links is provided in the main package -- see [here](#active-hyperlinks)) | +| `customText` | `CustomTextDefinitions` | | In addition to [localising the component](#localisation) text strings, you can also *dynamically* alter it, depending on the data. See [Custom Text](#custom-text) for more detail. | ## Update functions @@ -159,6 +164,7 @@ The function receives the following object: value, // value of the property size , // if a collection (object, array), the number of items (null for non-collections) parentData, // parent object containing the current node + fullData // the full (overall) data object collapsed // whether or not the current node is in a // "collapsed" state (only for Collection nodes) } @@ -220,6 +226,33 @@ restrictTypeSelection = { ({ path, value }) => { } } ``` +## Search/Filtering + +The displayed data can be filtered based on search input from a user. The user input should be captured independently (we don't provide a UI here) and passed in with the `searchText` prop. This input is debounced internally (time can be set with the `searchDebounceTime` prop), so no need for that as well. The values that the `searchText` are tested against is specified with the `searchFilter` prop. By default (no `searchFilter` defined), it will match against the data *values* (with case-insensitive partial matching -- i.e. input "Ilb", will match value "Bilbo"). + +You can specify what should be matched by setting `searchFilter` to either `"key"` (match property names), `"value"` (the default described above), or `"all"` (match both properties and values). This should be enough for the majority of use cases, but you can specify your own `SearchFilterFunction`. The search function is the same signature as the above [FilterFunctions](#filter-functions) but takes one additional argument for the `searchText`, i.e. + +```ts +( { key, path, level, value, ...etc }:FilterFunctionInput, searchText:string ) => boolean +``` + +There are two helper functions (`matchNode()` and `matchNodeKey()`) exported with the package that might make creating your search function easier (these are the functions used internally for the `"key"` and `"value"` matches described above). You can see what they do [here](#linktocodebase). + +An example custom search function can be seen in the [Demo](#https://carlosnz.github.io/json-edit-react/) with the "Client list" data set -- the search function matches by name and username, and makes the entire "Client" object visible when one of those matches, so it can be used to find a particular person and edit their specific details: + +```js +({ path, fullData }, searchText) => { + // Matches *any* node that shares a path (i.e. a descendent) with a matching name/username + if (path?.length >= 2) { + const index = path?.[0] + return ( + matchNode({ value: fullData[index].name }, searchText) || + matchNode({ value: fullData[index].username }, searchText) + ) + } else return false + } +``` + ## Themes & Styles There is a small selection of built-in themes (as seen in the [Demo app](https://carlosnz.github.io/json-edit-react/)). In order to use one of these, just pass the name into the `theme` prop (although realistically, these exist more to showcase the capabilities — I'm open to better built-in themes, so feel free to [create an issue](https://github.com/CarlosNZ/json-edit-react/issues) with suggestions). The available themes are: @@ -466,6 +499,7 @@ A few helper functions, components and types that might be useful in your own im - `LinkCustomComponent`: the component used to render [hyperlinks](#active-hyperlinks) - `LinkCustomNodeDefinition`: custom node definition for [hyperlinks](#active-hyperlinks) - `IconAdd`, `IconEdit`, `IconDelete`, `IconCopy`, `IconOk`, `IconCancel`, `IconChevron`: all the built-in [icon](#icons) components +- `matchNode`, `matchNodeKey`: helpers for defining custom [Search](#searchfiltering) functions - `truncate`: function to truncate a string to a specified length. See [here](https://github.com/CarlosNZ/json-edit-react/blob/d5fdbdfed6da7152f5802c67fbb3577810d13adc/src/ValueNodes.tsx#L9-L13) - `breakString`: function to turn a string into HTML, preserving line breaks and white space. See [here](https://github.com/CarlosNZ/json-edit-react/blob/d5fdbdfed6da7152f5802c67fbb3577810d13adc/src/ValueNodes.tsx#L15-L29) - `extract`: function to extract a deeply nested object value from a string path. See [here](https://github.com/CarlosNZ/object-property-extractor) @@ -477,7 +511,7 @@ A few helper functions, components and types that might be useful in your own im - `Theme`: a full [Theme](#themes--styles) object - `ThemeInput`: input type for the `theme` prop - `JsonEditorProps`: all input props for the Json Editor component -- [`UpdateFunction`](#update-functions), [`FilterFunction`](#filter-functions), [`CopyFunction`](#copy-function), [`CompareFunction`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort), [`LocalisedString`](#localisation), [`CustomNodeDefinition`](#custom-nodes), [`CustomTextDefinitions`](#custom-text) +- [`UpdateFunction`](#update-functions), [`FilterFunction`](#filter-functions), [`CopyFunction`](#copy-function), [`SearchFilterFunction`](#searchfiltering), [`CompareFunction`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort), [`LocalisedString`](#localisation), [`CustomNodeDefinition`](#custom-nodes), [`CustomTextDefinitions`](#custom-text) - `TranslateFunction`: function that takes a [localisation](#localisation) key and returns a translated string - `IconReplacements`: input type for the `icons` prop - `CollectionNodeProps`: all props passed internally to "collection" nodes (i.e. objects/arrays) @@ -494,7 +528,7 @@ The main features I'd like to introduce are: 1. **JSON Schema validation**. We can currently specify a reasonable degree of control over what can be edited using [Filter functions](#filter-functions) with the restriction props, but I'd like to go a step further and be able to pass in a [JSON Schema](https://json-schema.org/) and have the data be automatically validated against it, with the results reflected in the UI. This would allow control over data types and prevent missing properties, something that is not currently possible. 2. **Visibility filter function** — *hide* properties from the UI completely based on a Filter function. This should arguably be done outside the component though (filter the data upstream), so would be less of a priority (though it would be fairly simple to implement, so 🤷‍♂️) -3. **Search** — allow the user to narrow the list of visible keys with a simple search input. This would be useful for very large data objects, but is possibly getting a bit too much in terms of opinionated UI, so would need to ensure it can be styled easily. Perhaps it would be better if the "Search" input was handled outside this package, and we just accepted a "search" string prop? +3. ~~**Search** — allow the user to narrow the list of visible keys with a simple search input. This would be useful for very large data objects, but is possibly getting a bit too much in terms of opinionated UI, so would need to ensure it can be styled easily. Perhaps it would be better if the "Search" input was handled outside this package, and we just accepted a "search" string prop?~~ 👍 [Done](#searchfiltering) ## Inspiration @@ -502,6 +536,7 @@ This component is heavily inspired by [react-json-view](https://github.com/mac-s ## Changelog +- **1.7.0**: Implement [Search/filtering](#searchfiltering) of data visibility - **1.6.1**: Revert data state on Update Function error - **1.6.0**: Allow a function for `defaultValue` prop - **1.5.0**: diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 13d1f892..cce935b7 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -1,7 +1,7 @@ import 'react-datepicker/dist/react-datepicker.css' import React, { useEffect, useRef } from 'react' -import { JsonEditor, themes, ThemeName, Theme, assign } from './JsonEditImport' +import { JsonEditor, themes, ThemeName, Theme, FilterFunction } from './JsonEditImport' import { FaNpm, FaExternalLinkAlt, FaGithub } from 'react-icons/fa' import { BiReset } from 'react-icons/bi' import { AiOutlineCloudUpload } from 'react-icons/ai' @@ -36,7 +36,6 @@ import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons' import { demoData } from './demoData' import { useDatabase } from './useDatabase' import './style.css' -import { FilterFunction } from './json-edit-react/src/types' import { version } from './version' function App() { @@ -54,6 +53,7 @@ function App() { const [showIndices, setShowIndices] = useState(true) const [defaultNewValue, setDefaultNewValue] = useState('New data!') const [isSaving, setIsSaving] = useState(false) + const [searchText, setSearchText] = useState('') const previousThemeName = useRef('') // Used when resetting after theme editing const toast = useToast() @@ -105,6 +105,7 @@ function App() { const handleChangeData = (e) => { setSelectedData(e.target.value) + setSearchText('') if (e.target.value === 'editTheme') { previousThemeName.current = theme as string setCollapseLevel(demoData.editTheme.collapse as number) @@ -121,6 +122,7 @@ function App() { } const handleReset = async () => { + setSearchText('') switch (selectedData) { case 'editTheme': reset(themes[previousThemeName.current]) @@ -202,67 +204,87 @@ function App() { Demo - { - // if (newValue === 'wrong') return 'NOPE' - // } - demoData[selectedData]?.onUpdate - ? demoData[selectedData]?.onUpdate - : ({ newData }) => { - setData(newData) - if (selectedData === 'editTheme') setTheme(newData as ThemeName | Theme) - } - } - onEdit={ - demoData[selectedData]?.onEdit - ? (data) => { - const updateData = (demoData[selectedData] as any).onEdit(data) - if (updateData) setData(updateData) - } - : undefined - } - onAdd={ - demoData[selectedData]?.onAdd - ? (data) => { - const updateData = (demoData[selectedData] as any).onAdd(data) - if (updateData) setData(updateData) - } - : undefined - } - collapse={collapseLevel} - showCollectionCount={ - showCount === 'Yes' ? true : showCount === 'When closed' ? 'when-closed' : false - } - enableClipboard={ - allowCopy - ? ({ stringValue, type }) => - toast({ - title: `${type === 'value' ? 'Value' : 'Path'} copied to clipboard:`, - description: truncate(String(stringValue)), - status: 'success', - duration: 5000, - isClosable: true, - }) - : false - } - restrictEdit={restrictEdit} - restrictDelete={restrictDelete} - restrictAdd={restrictAdd} - restrictTypeSelection={demoData[selectedData]?.restrictTypeSelection} - keySort={sortKeys} - defaultValue={demoData[selectedData]?.defaultValue ?? defaultNewValue} - showArrayIndices={showIndices} - maxWidth="min(650px, 90vw)" - className="block-shadow" - stringTruncate={90} - customNodeDefinitions={demoData[selectedData]?.customNodeDefinitions} - customText={demoData[selectedData]?.customTextDefinitions} - /> + + setSearchText(e.target.value)} + position="absolute" + right={2} + top={2} + zIndex={100} + /> + { + setData(newData) + if (selectedData === 'editTheme') setTheme(newData as ThemeName | Theme) + } + } + onEdit={ + demoData[selectedData]?.onEdit + ? (data) => { + const updateData = (demoData[selectedData] as any).onEdit(data) + if (updateData) setData(updateData) + } + : undefined + } + onAdd={ + demoData[selectedData]?.onAdd + ? (data) => { + const updateData = (demoData[selectedData] as any).onAdd(data) + if (updateData) setData(updateData) + } + : undefined + } + collapse={collapseLevel} + showCollectionCount={ + showCount === 'Yes' ? true : showCount === 'When closed' ? 'when-closed' : false + } + enableClipboard={ + allowCopy + ? ({ stringValue, type }) => + toast({ + title: `${type === 'value' ? 'Value' : 'Path'} copied to clipboard:`, + description: truncate(String(stringValue)), + status: 'success', + duration: 5000, + isClosable: true, + }) + : false + } + restrictEdit={restrictEdit} + restrictDelete={restrictDelete} + restrictAdd={restrictAdd} + restrictTypeSelection={demoData[selectedData]?.restrictTypeSelection} + searchFilter={demoData[selectedData]?.searchFilter} + searchText={searchText} + keySort={sortKeys} + defaultValue={demoData[selectedData]?.defaultValue ?? defaultNewValue} + showArrayIndices={showIndices} + minWidth={450} + maxWidth="min(650px, 90vw)" + className="block-shadow" + stringTruncate={90} + customNodeDefinitions={demoData[selectedData]?.customNodeDefinitions} + customText={demoData[selectedData]?.customTextDefinitions} + /> +