Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/utils-filter-toolkit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@json-edit-react/utils': minor
---

Add a filter-function toolkit — composable predicate builders for the `allow*` props (`allowEdit`, `allowDelete`, `allowAdd`, `allowTypeSelection`, …) and `searchFilter`.

Every builder returns the same `FilterPredicate` shape — `(node, searchText?) => boolean` — whose optional second argument makes it assignable to both `FilterFunction` (the `allow*` props) and `SearchFilterFunction` (`searchFilter`), so one set of builders serves every filter prop. Property builders: `byKey`, `byPath` (glob / RegExp / segment-array paths), `byLevel`, `bySize`, `byType`, `byValue`. Position constants: `root`, `collections`, `primitives`, `inArray`, `inObject`. Combinators: `and` / `or` / `not` (they thread `searchText`, so search bridges compose with structural builders). Search bridges: `matchesSearch(mode?)` (wraps core's own matchers) and `matchRecord({ fields, path? })` (reveals a whole record when one of its fields matches, instead of collapsing it to the single matching field).

Each builder interns its result, so equal arguments return the same instance — you can write a builder inline on a prop (`allowEdit={byKey('id')}`) without a `useMemo` or hoisting, and it won't churn json-edit-react's fine-grained re-rendering. No third-party runtime dependencies; the search bridges reuse core's exported `matchNode` / `matchNodeKey` / `extract`. See `src/filters/README.md` for the full reference, including the glob path syntax.
11 changes: 11 additions & 0 deletions .github/workflows/pr-bundle-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
core: ${{ steps.filter.outputs.core }}
themes: ${{ steps.filter.outputs.themes }}
components: ${{ steps.filter.outputs.components }}
utils: ${{ steps.filter.outputs.utils }}
any: ${{ steps.filter.outputs.changes != '[]' }}
steps:
- uses: actions/checkout@v4
Expand All @@ -35,6 +36,8 @@ jobs:
- 'packages/themes/**'
components:
- 'packages/components/**'
utils:
- 'packages/utils/**'

size:
needs: detect
Expand Down Expand Up @@ -81,6 +84,9 @@ jobs:
if [[ "${{ needs.detect.outputs.components }}" == "true" ]]; then
pnpm --filter @json-edit-react/components build
fi
if [[ "${{ needs.detect.outputs.utils }}" == "true" ]]; then
pnpm --filter @json-edit-react/utils build
fi

- name: Measure PR sizes
working-directory: pr
Expand All @@ -89,6 +95,7 @@ jobs:
MEASURE_CORE: ${{ needs.detect.outputs.core }}
MEASURE_THEMES: ${{ needs.detect.outputs.themes }}
MEASURE_COMPONENTS: ${{ needs.detect.outputs.components }}
MEASURE_UTILS: ${{ needs.detect.outputs.utils }}

- name: Install (base)
working-directory: base
Expand All @@ -106,6 +113,9 @@ jobs:
if [[ "${{ needs.detect.outputs.components }}" == "true" ]]; then
pnpm --filter @json-edit-react/components build
fi
if [[ "${{ needs.detect.outputs.utils }}" == "true" ]]; then
pnpm --filter @json-edit-react/utils build
fi

- name: Measure base sizes
working-directory: base
Expand All @@ -114,6 +124,7 @@ jobs:
MEASURE_CORE: ${{ needs.detect.outputs.core }}
MEASURE_THEMES: ${{ needs.detect.outputs.themes }}
MEASURE_COMPONENTS: ${{ needs.detect.outputs.components }}
MEASURE_UTILS: ${{ needs.detect.outputs.utils }}

- name: Format comment
run: node pr/scripts/format-size-diff.mjs base-sizes.json pr-sizes.json > comment.md
Expand Down
72 changes: 43 additions & 29 deletions demo/src/demoData/dataDefinitions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ import {
UpdateFunctionProps,
type AssignInput,
} from '@json-edit-react'
import {
and,
byKey,
byLevel,
byType,
matchRecord,
not,
primitives,
root,
} from '@json-edit-react/utils'
import jsonSchema from './jsonSchema.json'
import customNodesSchema from './customNodesSchema.json'
import Ajv from 'ajv'
Expand Down Expand Up @@ -172,9 +182,12 @@ export const demoDataDefinitions: Record<string, DemoData> = {
</Flex>
),
rootName: 'Star Wars data',
allowEdit: ({ value }) => typeof value !== 'object' || value === null,
allowDelete: ({ value }) => typeof value !== 'object' || value === null,
allowAdd: ({ value }) => Array.isArray(value),
// allowEdit: ({ value }) => typeof value !== 'object' || value === null,
allowEdit: primitives,
// allowDelete: ({ value }) => typeof value !== 'object' || value === null,
allowDelete: primitives,
// allowAdd: ({ value }) => Array.isArray(value),
allowAdd: byType('array'),
allowTypeSelection: ({ key, path }) => {
if (path.slice(-2)[0] === 'films' || (path.slice(-3)[0] === 'films' && key === 'title'))
return [
Expand Down Expand Up @@ -301,20 +314,14 @@ export const demoDataDefinitions: Record<string, DemoData> = {
</Flex>
),
rootName: 'Clients',
allowEdit: ({ key, level }) => key !== 'id' && level !== 0 && level !== 1,
allowAdd: ({ level }) => level !== 1,
allowDelete: ({ level }) => level === 1,
// allowEdit: ({ key, level }) => key !== 'id' && level !== 0 && level !== 1,
allowEdit: and(not(byKey('id')), byLevel({ min: 2 })),
// allowAdd: ({ level }) => level !== 1,
allowAdd: not(byLevel(1)),
// allowDelete: ({ level }) => level === 1,
allowDelete: byLevel(1),
collapse: 2,
searchFilter: ({ path, fullData }, searchText) => {
const data = fullData as { name: string; username: string }[]
if (path?.length >= 2) {
const index = path?.[0] as number
return (
matchNode({ value: data[index].name }, searchText) ||
matchNode({ value: data[index].username }, searchText)
)
} else return false
},
searchFilter: matchRecord({ fields: ['name', 'username'] }),
searchPlaceholder: 'Search by name or username',
defaultValue: ({ level }) => {
if (level === 0)
Expand Down Expand Up @@ -610,9 +617,12 @@ export const demoDataDefinitions: Record<string, DemoData> = {
</Flex>
),
rootName: 'theme',
allowEdit: ({ key, level }) => level !== 0 && !['fragments', 'styles'].includes(key as string),
allowDelete: ({ key }) => !['displayName', 'fragments', 'styles'].includes(key as string),
allowAdd: ({ level }) => level !== 0,
// allowEdit: ({ key, level }) => level !== 0 && !['fragments', 'styles'].includes(key as string),
allowEdit: and(not(root), not(byKey('fragments', 'styles'))),
// allowDelete: ({ key }) => !['displayName', 'fragments', 'styles'].includes(key as string),
allowDelete: not(byKey('displayName', 'fragments', 'styles')),
// allowAdd: ({ level }) => level !== 0,
allowAdd: not(root),
allowTypeSelection: ['string', 'object', 'array'],
collapse: 2,
searchFilter: 'key',
Expand Down Expand Up @@ -667,16 +677,18 @@ export const demoDataDefinitions: Record<string, DemoData> = {
),
rootName: 'Superheroes',
collapse: 2,
searchFilter: ({ path, fullData }, searchText = '') => {
const data = fullData as { name: string }[]
if (path?.length >= 2) {
const index = path?.[0] as number
return matchNode({ value: data[index].name }, searchText)
} else return false
},
// searchFilter: ({ path, fullData }, searchText = '') => {
// const data = fullData as { name: string }[]
// if (path?.length >= 2) {
// const index = path?.[0] as number
// return matchNode({ value: data[index].name }, searchText)
// } else return false
// },
searchFilter: matchRecord({ fields: ['name'] }),
searchPlaceholder: 'Search by character name',
data: data.customNodes,
allowEdit: ({ level }) => level === 0,
// allowEdit: ({ level }) => level === 0,
allowEdit: root,
allowAdd: false,
allowDelete: false,
onUpdate: ({ newData }, toast) => {
Expand Down Expand Up @@ -867,8 +879,10 @@ export const demoDataDefinitions: Record<string, DemoData> = {
rootName: 'dossier',
collapse: 2,
data: data.customKeys,
allowAdd: ({ level }) => level !== 0,
allowDelete: ({ level }) => level !== 0,
// allowAdd: ({ level }) => level !== 0,
allowAdd: not(root),
// allowDelete: ({ level }) => level !== 0,
allowDelete: not(root),
customNodeDefinitions: [
// 1. "REDACTED_" prefix — blacked-out key, original visible on hover.
// Must come before the `_` matcher (which would still match these
Expand Down
1 change: 1 addition & 0 deletions demo/src/examples/example-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
| External control | | |
| Custom components | | | One that demonstrates all the types and props |
| Custom components | | | One that demonstrates all the types and props |
| Filter toolkit | ✅️ | | Predicate builders from `@json-edit-react/utils`: highlight matches via a theme layer, bind to `allowEdit`, `matchesSearch`/`matchRecord` bridges; org-chart data |
| Search utilities | | | Demonstrate "Search" utils |
| JsonViewer | | | |
| Validation staleness | ✅️ | | `devOnly` scratchpad — naive style-fn validation under fine-grained re-rendering |
Expand Down
Loading
Loading