Skip to content

Commit b0cea96

Browse files
authored
#11 Search/filtering (#39)
1 parent 6007f6c commit b0cea96

12 files changed

+399
-122
lines changed

README.md

+65-30
Large diffs are not rendered by default.

demo/src/App.tsx

+85-63
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'react-datepicker/dist/react-datepicker.css'
22

33
import React, { useEffect, useRef } from 'react'
4-
import { JsonEditor, themes, ThemeName, Theme, assign } from './JsonEditImport'
4+
import { JsonEditor, themes, ThemeName, Theme, FilterFunction } from './JsonEditImport'
55
import { FaNpm, FaExternalLinkAlt, FaGithub } from 'react-icons/fa'
66
import { BiReset } from 'react-icons/bi'
77
import { AiOutlineCloudUpload } from 'react-icons/ai'
@@ -36,7 +36,6 @@ import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons'
3636
import { demoData } from './demoData'
3737
import { useDatabase } from './useDatabase'
3838
import './style.css'
39-
import { FilterFunction } from './json-edit-react/src/types'
4039
import { version } from './version'
4140

4241
function App() {
@@ -54,6 +53,7 @@ function App() {
5453
const [showIndices, setShowIndices] = useState(true)
5554
const [defaultNewValue, setDefaultNewValue] = useState('New data!')
5655
const [isSaving, setIsSaving] = useState(false)
56+
const [searchText, setSearchText] = useState('')
5757
const previousThemeName = useRef('') // Used when resetting after theme editing
5858
const toast = useToast()
5959

@@ -105,6 +105,7 @@ function App() {
105105

106106
const handleChangeData = (e) => {
107107
setSelectedData(e.target.value)
108+
setSearchText('')
108109
if (e.target.value === 'editTheme') {
109110
previousThemeName.current = theme as string
110111
setCollapseLevel(demoData.editTheme.collapse as number)
@@ -121,6 +122,7 @@ function App() {
121122
}
122123

123124
const handleReset = async () => {
125+
setSearchText('')
124126
switch (selectedData) {
125127
case 'editTheme':
126128
reset(themes[previousThemeName.current])
@@ -202,67 +204,87 @@ function App() {
202204
<Heading size="lg" variant="accent">
203205
Demo
204206
</Heading>
205-
<JsonEditor
206-
data={data}
207-
rootName={rootName}
208-
theme={[theme, demoData[selectedData]?.styles ?? {}]}
209-
indent={indent}
210-
onUpdate={
211-
// ({ newValue }) => {
212-
// if (newValue === 'wrong') return 'NOPE'
213-
// }
214-
demoData[selectedData]?.onUpdate
215-
? demoData[selectedData]?.onUpdate
216-
: ({ newData }) => {
217-
setData(newData)
218-
if (selectedData === 'editTheme') setTheme(newData as ThemeName | Theme)
219-
}
220-
}
221-
onEdit={
222-
demoData[selectedData]?.onEdit
223-
? (data) => {
224-
const updateData = (demoData[selectedData] as any).onEdit(data)
225-
if (updateData) setData(updateData)
226-
}
227-
: undefined
228-
}
229-
onAdd={
230-
demoData[selectedData]?.onAdd
231-
? (data) => {
232-
const updateData = (demoData[selectedData] as any).onAdd(data)
233-
if (updateData) setData(updateData)
234-
}
235-
: undefined
236-
}
237-
collapse={collapseLevel}
238-
showCollectionCount={
239-
showCount === 'Yes' ? true : showCount === 'When closed' ? 'when-closed' : false
240-
}
241-
enableClipboard={
242-
allowCopy
243-
? ({ stringValue, type }) =>
244-
toast({
245-
title: `${type === 'value' ? 'Value' : 'Path'} copied to clipboard:`,
246-
description: truncate(String(stringValue)),
247-
status: 'success',
248-
duration: 5000,
249-
isClosable: true,
250-
})
251-
: false
252-
}
253-
restrictEdit={restrictEdit}
254-
restrictDelete={restrictDelete}
255-
restrictAdd={restrictAdd}
256-
restrictTypeSelection={demoData[selectedData]?.restrictTypeSelection}
257-
keySort={sortKeys}
258-
defaultValue={demoData[selectedData]?.defaultValue ?? defaultNewValue}
259-
showArrayIndices={showIndices}
260-
maxWidth="min(650px, 90vw)"
261-
className="block-shadow"
262-
stringTruncate={90}
263-
customNodeDefinitions={demoData[selectedData]?.customNodeDefinitions}
264-
customText={demoData[selectedData]?.customTextDefinitions}
265-
/>
207+
<Box position="relative">
208+
<Input
209+
placeholder={demoData[selectedData].searchPlaceholder ?? 'Search values'}
210+
bgColor={'#f6f6f6'}
211+
borderColor="gainsboro"
212+
borderRadius={50}
213+
size="sm"
214+
w={60}
215+
value={searchText}
216+
onChange={(e) => setSearchText(e.target.value)}
217+
position="absolute"
218+
right={2}
219+
top={2}
220+
zIndex={100}
221+
/>
222+
<JsonEditor
223+
data={data}
224+
rootName={rootName}
225+
theme={[
226+
theme,
227+
demoData[selectedData]?.styles ?? {},
228+
{ container: { paddingTop: '1em' } },
229+
]}
230+
indent={indent}
231+
onUpdate={
232+
demoData[selectedData]?.onUpdate
233+
? demoData[selectedData]?.onUpdate
234+
: ({ newData }) => {
235+
setData(newData)
236+
if (selectedData === 'editTheme') setTheme(newData as ThemeName | Theme)
237+
}
238+
}
239+
onEdit={
240+
demoData[selectedData]?.onEdit
241+
? (data) => {
242+
const updateData = (demoData[selectedData] as any).onEdit(data)
243+
if (updateData) setData(updateData)
244+
}
245+
: undefined
246+
}
247+
onAdd={
248+
demoData[selectedData]?.onAdd
249+
? (data) => {
250+
const updateData = (demoData[selectedData] as any).onAdd(data)
251+
if (updateData) setData(updateData)
252+
}
253+
: undefined
254+
}
255+
collapse={collapseLevel}
256+
showCollectionCount={
257+
showCount === 'Yes' ? true : showCount === 'When closed' ? 'when-closed' : false
258+
}
259+
enableClipboard={
260+
allowCopy
261+
? ({ stringValue, type }) =>
262+
toast({
263+
title: `${type === 'value' ? 'Value' : 'Path'} copied to clipboard:`,
264+
description: truncate(String(stringValue)),
265+
status: 'success',
266+
duration: 5000,
267+
isClosable: true,
268+
})
269+
: false
270+
}
271+
restrictEdit={restrictEdit}
272+
restrictDelete={restrictDelete}
273+
restrictAdd={restrictAdd}
274+
restrictTypeSelection={demoData[selectedData]?.restrictTypeSelection}
275+
searchFilter={demoData[selectedData]?.searchFilter}
276+
searchText={searchText}
277+
keySort={sortKeys}
278+
defaultValue={demoData[selectedData]?.defaultValue ?? defaultNewValue}
279+
showArrayIndices={showIndices}
280+
minWidth={450}
281+
maxWidth="min(650px, 90vw)"
282+
className="block-shadow"
283+
stringTruncate={90}
284+
customNodeDefinitions={demoData[selectedData]?.customNodeDefinitions}
285+
customText={demoData[selectedData]?.customTextDefinitions}
286+
/>
287+
</Box>
266288
<VStack w="100%" align="flex-end" gap={4}>
267289
<HStack w="100%" justify="space-between" mt={4}>
268290
<Button

demo/src/JsonEditImport.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import {
1010
FilterFunction,
1111
LinkCustomComponent,
1212
LinkCustomNodeDefinition,
13+
matchNode,
1314
assign,
14-
// } from './json-edit-react/src'
15-
} from 'json-edit-react'
15+
} from './json-edit-react/src'
16+
// } from 'json-edit-react'
1617
// } from './package'
1718

1819
export {
@@ -27,5 +28,6 @@ export {
2728
type FilterFunction,
2829
LinkCustomComponent,
2930
LinkCustomNodeDefinition,
31+
matchNode,
3032
assign,
3133
}

demo/src/demoData/dataDefinitions.tsx

+80-16
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import {
88
CustomTextDefinitions,
99
LinkCustomNodeDefinition,
1010
assign,
11+
matchNode,
1112
} from '../JsonEditImport'
1213
import {
1314
CollectionKey,
1415
DataType,
1516
DefaultValueFunction,
17+
SearchFilterFunction,
1618
ThemeStyles,
1719
UpdateFunction,
1820
} from '../json-edit-react/src/types'
@@ -28,6 +30,8 @@ interface DemoData {
2830
restrictDelete?: FilterFunction
2931
restrictAdd?: FilterFunction
3032
restrictTypeSelection?: boolean | DataType[]
33+
searchFilter?: 'key' | 'value' | 'all' | SearchFilterFunction
34+
searchPlaceholder?: string
3135
onUpdate?: UpdateFunction
3236
onAdd?: (props: {
3337
newData: object
@@ -141,16 +145,36 @@ export const demoData: Record<string, DemoData> = {
141145
to the main "Person" objects.
142146
</Text>
143147
<Text>
144-
Also, notice that when a new item is added at the top level, a correctly structured{' '}
148+
Also, notice that when you add a new item in the top level array, a correctly structured{' '}
145149
"Person" object is added, but adding new items elsewhere adds simple string values. This
146150
is done by specifying a function for the <span className="code">defaultValue</span> prop.
147151
</Text>
152+
<Text>
153+
We've also changed the behaviour of the "Search" input, so that it matches specific people
154+
(on <span className="code">name</span> and <span className="code">username</span>) and
155+
displays all fields associated with the matching people. This is achieved by specifying a
156+
custom{' '}
157+
<Link href="https://github.com./CarlosNZ/json-edit-react#searchfiltering" isExternal>
158+
Search filter function
159+
</Link>
160+
.
161+
</Text>
148162
</Flex>
149163
),
150164
restrictEdit: ({ key, level }) => key === 'id' || level === 0 || level === 1,
151165
restrictAdd: ({ level }) => level === 1,
152166
restrictDelete: ({ key }) => key === 'id',
153167
collapse: 2,
168+
searchFilter: ({ path, fullData }, searchText) => {
169+
if (path?.length >= 2) {
170+
const index = path?.[0]
171+
return (
172+
matchNode({ value: fullData[index].name }, searchText) ||
173+
matchNode({ value: fullData[index].username }, searchText)
174+
)
175+
} else return false
176+
},
177+
searchPlaceholder: 'Search by name or username',
154178
defaultValue: ({ level }) => {
155179
if (level === 0)
156180
return {
@@ -183,51 +207,70 @@ export const demoData: Record<string, DemoData> = {
183207
vsCode: {
184208
name: '⚙️ VSCode settings file',
185209
description: (
186-
<>
210+
<Flex flexDir="column" gap={2}>
187211
<Text>
188212
A typical{' '}
189213
<Link href="https://code.visualstudio.com/" isExternal>
190214
VSCode
191215
</Link>{' '}
192216
config file.
193217
</Text>
194-
<Text mt={3}>
218+
<Text>
195219
The only restriction here is that you can't set any boolean values to{' '}
196220
<span className="code">false</span>. It uses a custom{' '}
197221
<span className="code">onUpdate</span> function to return an error string when you attempt
198222
to do so, and the value is reset to <span className="code">true</span>.
199223
</Text>
200-
</>
224+
<Text>
225+
Note the "Search" input is configured to filter for object <em>properties</em> rather than{' '}
226+
<em>values</em> (by setting <span className="code">searchFilter: "key"</span>).
227+
</Text>
228+
</Flex>
201229
),
202230
collapse: 2,
203231
data: data.vsCode,
204232
onUpdate: ({ newValue }) => {
205233
if (newValue === false) return "Don't use FALSE, just delete the value"
206234
},
235+
searchFilter: 'key',
236+
searchPlaceholder: 'Search properties',
207237
},
208238
liveData: {
209239
name: '📖 Live Data (from database)',
210240
description: (
211-
<>
241+
<Flex flexDir="column" gap={2}>
212242
<Text>
213243
Here's a live "guestbook" — your changes can be saved permanently to the cloud. However,
214244
there are restrictions:
215-
<UnorderedList>
216-
<ListItem>You can only add new messages, or fields within your message</ListItem>
217-
<ListItem>Only the most recent message is editable, and only for five minutes</ListItem>
218-
</UnorderedList>
219245
</Text>
220-
<Text mt={3}>
246+
<UnorderedList>
247+
<ListItem>
248+
<Text>You can only add new messages, or fields within your message</Text>
249+
</ListItem>
250+
<ListItem>
251+
<Text>Only the most recent message is editable, and only for five minutes</Text>
252+
</ListItem>
253+
</UnorderedList>
254+
<Text>
221255
Notice also (these are achieved by customising the <span className="code">onEdit</span>{' '}
222256
and <span className="code">onAdd</span> props):
223-
<UnorderedList>
224-
<ListItem>
257+
</Text>
258+
<UnorderedList>
259+
<ListItem>
260+
<Text>
225261
The messages list gets sorted so the most recent is at the <em>top</em>
226-
</ListItem>
227-
<ListItem>The timestamps get updated automatically after each edit</ListItem>
228-
</UnorderedList>
262+
</Text>
263+
</ListItem>
264+
<ListItem>
265+
<Text>The timestamps get updated automatically after each edit</Text>
266+
</ListItem>
267+
</UnorderedList>
268+
<Text>
269+
You can also filter full "Message" objects by searching any text value (
270+
<span className="code">message</span>, <span className="code">name</span>,{' '}
271+
<span className="code">from</span>).
229272
</Text>
230-
</>
273+
</Flex>
231274
),
232275
rootName: 'liveData',
233276
collapse: 3,
@@ -290,6 +333,18 @@ export const demoData: Record<string, DemoData> = {
290333
}
291334
return 'New value'
292335
},
336+
searchFilter: ({ path, fullData }, searchText) => {
337+
if (path?.length >= 2 && path[0] === 'messages') {
338+
const index = path?.[1]
339+
const messages = (fullData as { messages: unknown[] })?.messages
340+
return (
341+
matchNode({ value: messages[index].message }, searchText) ||
342+
matchNode({ value: messages[index].name }, searchText) ||
343+
matchNode({ value: messages[index].from }, searchText)
344+
)
345+
} else return true
346+
},
347+
searchPlaceholder: 'Search guestbook',
293348
data: {},
294349
customNodeDefinitions: [
295350
{
@@ -337,6 +392,8 @@ export const demoData: Record<string, DemoData> = {
337392
restrictAdd: ({ level }) => level === 0,
338393
restrictTypeSelection: ['string', 'object', 'array'],
339394
collapse: 2,
395+
searchFilter: 'key',
396+
searchPlaceholder: 'Search Theme keys',
340397
data: {},
341398
},
342399
customNodes: {
@@ -373,6 +430,13 @@ export const demoData: Record<string, DemoData> = {
373430
),
374431
rootName: 'Superheroes',
375432
collapse: 2,
433+
searchFilter: ({ path, fullData }, searchText = '') => {
434+
if (path?.length >= 2) {
435+
const index = path?.[0]
436+
return matchNode({ value: fullData[index].name }, searchText)
437+
} else return false
438+
},
439+
searchPlaceholder: 'Search by character name',
376440
data: data.customNodes,
377441
customNodeDefinitions: [
378442
{

0 commit comments

Comments
 (0)