@byline/admin 3.2.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/forms/form-context.js +2 -2
- package/dist/forms/nested-path.d.ts +24 -0
- package/dist/forms/nested-path.js +29 -0
- package/dist/forms/nested-path.test.node.d.ts +8 -0
- package/package.json +5 -7
- package/src/forms/form-context.tsx +6 -1
- package/src/forms/nested-path.test.node.ts +85 -0
- package/src/forms/nested-path.ts +60 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { normalizeHooks } from "@byline/core";
|
|
5
|
-
import { get, set as
|
|
5
|
+
import { get, set as external_nested_path_js_set } from "./nested-path.js";
|
|
6
6
|
const sameLocaleSet = (a, b)=>{
|
|
7
7
|
if (a.length !== b.length) return false;
|
|
8
8
|
const sa = [
|
|
@@ -84,7 +84,7 @@ const FormProvider = ({ children, initialData = {} })=>{
|
|
|
84
84
|
const newFieldValues = {
|
|
85
85
|
...fieldValues.current
|
|
86
86
|
};
|
|
87
|
-
|
|
87
|
+
external_nested_path_js_set(newFieldValues, name, value);
|
|
88
88
|
fieldValues.current = newFieldValues;
|
|
89
89
|
dirtyFields.current.add(name);
|
|
90
90
|
notifyFieldListeners(name, value);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*
|
|
8
|
+
* Minimal nested `get`/`set` over string field paths, replacing lodash-es
|
|
9
|
+
* (which pulled a large shared chunk onto unrelated bundles). Supports the
|
|
10
|
+
* dot + bracket notation produced by the form field-path builders, e.g.
|
|
11
|
+
* `title`, `a.b.c`, `items[0].title`, `blocks[2].nested[1].field`.
|
|
12
|
+
*
|
|
13
|
+
* `set` mirrors lodash semantics: it creates intermediate **arrays** when the
|
|
14
|
+
* next path segment is a numeric index and plain **objects** otherwise, and it
|
|
15
|
+
* mutates `object` in place (callers shallow-copy the root first, as before).
|
|
16
|
+
*
|
|
17
|
+
* Deliberately NOT a general lodash replacement — it does not handle quoted
|
|
18
|
+
* keys (`a["b.c"]`), negative indices, or array-path inputs, none of which the
|
|
19
|
+
* form paths ever produce. See nested-path.test.node.ts for the covered cases.
|
|
20
|
+
*/
|
|
21
|
+
/** Split a field path into segments: `items[0].title` -> ['items','0','title']. */
|
|
22
|
+
export declare function toPath(path: string): string[];
|
|
23
|
+
export declare function get<T = any>(object: unknown, path: string): T;
|
|
24
|
+
export declare function set<T extends object>(object: T, path: string, value: unknown): T;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const isIndexKey = (key)=>/^(?:0|[1-9]\d*)$/.test(key);
|
|
2
|
+
function toPath(path) {
|
|
3
|
+
return path.match(/[^.[\]]+/g) ?? [];
|
|
4
|
+
}
|
|
5
|
+
function get(object, path) {
|
|
6
|
+
if (null == object) return;
|
|
7
|
+
let current = object;
|
|
8
|
+
for (const key of toPath(path)){
|
|
9
|
+
if (null == current) return;
|
|
10
|
+
current = current[key];
|
|
11
|
+
}
|
|
12
|
+
return current;
|
|
13
|
+
}
|
|
14
|
+
function set(object, path, value) {
|
|
15
|
+
if (null == object) return object;
|
|
16
|
+
const keys = toPath(path);
|
|
17
|
+
if (0 === keys.length) return object;
|
|
18
|
+
let current = object;
|
|
19
|
+
for(let i = 0; i < keys.length - 1; i++){
|
|
20
|
+
const key = keys[i];
|
|
21
|
+
const nextKey = keys[i + 1];
|
|
22
|
+
const existing = current[key];
|
|
23
|
+
if (null == existing || 'object' != typeof existing) current[key] = isIndexKey(nextKey) ? [] : {};
|
|
24
|
+
current = current[key];
|
|
25
|
+
}
|
|
26
|
+
current[keys[keys.length - 1]] = value;
|
|
27
|
+
return object;
|
|
28
|
+
}
|
|
29
|
+
export { get, set, toPath };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/admin",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "3.2.
|
|
5
|
+
"version": "3.2.1",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -142,15 +142,14 @@
|
|
|
142
142
|
"@tanstack/react-form-start": "^1.32.0",
|
|
143
143
|
"classnames": "^2.5.1",
|
|
144
144
|
"jose": "^6.2.3",
|
|
145
|
-
"lodash-es": "^4.18.1",
|
|
146
145
|
"react-diff-viewer-continued": "^4.2.2",
|
|
147
146
|
"uuid": "^14.0.0",
|
|
148
147
|
"zod": "^4.4.3",
|
|
149
148
|
"zod-form-data": "^3.0.1",
|
|
150
|
-
"@byline/
|
|
151
|
-
"@byline/
|
|
152
|
-
"@byline/ui": "3.2.
|
|
153
|
-
"@byline/
|
|
149
|
+
"@byline/core": "3.2.1",
|
|
150
|
+
"@byline/i18n": "3.2.1",
|
|
151
|
+
"@byline/ui": "3.2.1",
|
|
152
|
+
"@byline/auth": "3.2.1"
|
|
154
153
|
},
|
|
155
154
|
"peerDependencies": {
|
|
156
155
|
"react": "^19.0.0",
|
|
@@ -160,7 +159,6 @@
|
|
|
160
159
|
"@biomejs/biome": "2.4.15",
|
|
161
160
|
"@rsbuild/plugin-react": "^2.0.0",
|
|
162
161
|
"@rslib/core": "^0.21.5",
|
|
163
|
-
"@types/lodash-es": "^4.17.12",
|
|
164
162
|
"@types/node": "^25.9.1",
|
|
165
163
|
"@types/react": "19.2.15",
|
|
166
164
|
"@types/react-dom": "19.2.3",
|
|
@@ -14,7 +14,12 @@ import { createContext, useCallback, useContext, useEffect, useRef, useState } f
|
|
|
14
14
|
import type { Field, FieldBeforeChangeResult, FieldHookContext } from '@byline/core'
|
|
15
15
|
import { normalizeHooks } from '@byline/core'
|
|
16
16
|
import type { DocumentPatch, FieldSetPatch } from '@byline/core/patches'
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
// Vendored nested get/set (see ./nested-path) — removes the lodash-es dep
|
|
19
|
+
// outright. A bare `from 'lodash-es'` import otherwise pools into a single
|
|
20
|
+
// ~85KB chunk that leaks onto the public frontend bundle (form-context is
|
|
21
|
+
// reachable from the layout graph).
|
|
22
|
+
import { get as getNestedValue, set as setNestedValue } from './nested-path'
|
|
18
23
|
|
|
19
24
|
interface FormError {
|
|
20
25
|
field: string
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from 'vitest'
|
|
10
|
+
|
|
11
|
+
import { get, set, toPath } from './nested-path'
|
|
12
|
+
|
|
13
|
+
describe('toPath', () => {
|
|
14
|
+
it('parses dot and bracket notation', () => {
|
|
15
|
+
expect(toPath('title')).toEqual(['title'])
|
|
16
|
+
expect(toPath('a.b.c')).toEqual(['a', 'b', 'c'])
|
|
17
|
+
expect(toPath('items[0].title')).toEqual(['items', '0', 'title'])
|
|
18
|
+
expect(toPath('blocks[2].nested[1].field')).toEqual(['blocks', '2', 'nested', '1', 'field'])
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('get', () => {
|
|
23
|
+
const obj = { a: { b: 1, zero: 0, empty: '' }, items: [{ title: 'x' }, { title: 'y' }] }
|
|
24
|
+
|
|
25
|
+
it('reads nested, array, and mixed paths', () => {
|
|
26
|
+
expect(get(obj, 'a.b')).toBe(1)
|
|
27
|
+
expect(get(obj, 'items[0].title')).toBe('x')
|
|
28
|
+
expect(get(obj, 'items[1].title')).toBe('y')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('preserves falsy values (does not conflate with missing)', () => {
|
|
32
|
+
expect(get(obj, 'a.zero')).toBe(0)
|
|
33
|
+
expect(get(obj, 'a.empty')).toBe('')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('returns undefined for missing paths or nullish roots', () => {
|
|
37
|
+
expect(get(obj, 'a.x')).toBeUndefined()
|
|
38
|
+
expect(get(obj, 'missing.deep.path')).toBeUndefined()
|
|
39
|
+
expect(get(obj, 'items[5].title')).toBeUndefined()
|
|
40
|
+
expect(get(null, 'a.b')).toBeUndefined()
|
|
41
|
+
expect(get(undefined, 'a.b')).toBeUndefined()
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('set', () => {
|
|
46
|
+
it('sets simple and nested values, creating intermediate objects', () => {
|
|
47
|
+
const o: any = {}
|
|
48
|
+
set(o, 'a.b.c', 1)
|
|
49
|
+
expect(o).toEqual({ a: { b: { c: 1 } } })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('creates arrays for numeric index segments', () => {
|
|
53
|
+
const o: any = {}
|
|
54
|
+
set(o, 'items[0].title', 'x')
|
|
55
|
+
expect(Array.isArray(o.items)).toBe(true)
|
|
56
|
+
expect(o.items[0]).toEqual({ title: 'x' })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('handles deep, mixed array/object paths', () => {
|
|
60
|
+
const o: any = {}
|
|
61
|
+
set(o, 'blocks[1].nested[0].field', 42)
|
|
62
|
+
expect(Array.isArray(o.blocks)).toBe(true)
|
|
63
|
+
expect(Array.isArray(o.blocks[1].nested)).toBe(true)
|
|
64
|
+
expect(o.blocks[1].nested[0].field).toBe(42)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('overwrites existing values and preserves siblings', () => {
|
|
68
|
+
const o: any = { a: { b: 1, keep: 2 } }
|
|
69
|
+
set(o, 'a.b', 9)
|
|
70
|
+
expect(o).toEqual({ a: { b: 9, keep: 2 } })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('writes into a pre-existing array element without clobbering the array', () => {
|
|
74
|
+
const o: any = { items: [{ title: 'x' }, { title: 'y' }] }
|
|
75
|
+
set(o, 'items[1].title', 'z')
|
|
76
|
+
expect(o.items[1].title).toBe('z')
|
|
77
|
+
expect(o.items[0].title).toBe('x')
|
|
78
|
+
expect(Array.isArray(o.items)).toBe(true)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('returns the mutated root', () => {
|
|
82
|
+
const o: any = {}
|
|
83
|
+
expect(set(o, 'x', 1)).toBe(o)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*
|
|
8
|
+
* Minimal nested `get`/`set` over string field paths, replacing lodash-es
|
|
9
|
+
* (which pulled a large shared chunk onto unrelated bundles). Supports the
|
|
10
|
+
* dot + bracket notation produced by the form field-path builders, e.g.
|
|
11
|
+
* `title`, `a.b.c`, `items[0].title`, `blocks[2].nested[1].field`.
|
|
12
|
+
*
|
|
13
|
+
* `set` mirrors lodash semantics: it creates intermediate **arrays** when the
|
|
14
|
+
* next path segment is a numeric index and plain **objects** otherwise, and it
|
|
15
|
+
* mutates `object` in place (callers shallow-copy the root first, as before).
|
|
16
|
+
*
|
|
17
|
+
* Deliberately NOT a general lodash replacement — it does not handle quoted
|
|
18
|
+
* keys (`a["b.c"]`), negative indices, or array-path inputs, none of which the
|
|
19
|
+
* form paths ever produce. See nested-path.test.node.ts for the covered cases.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const isIndexKey = (key: string): boolean => /^(?:0|[1-9]\d*)$/.test(key)
|
|
23
|
+
|
|
24
|
+
/** Split a field path into segments: `items[0].title` -> ['items','0','title']. */
|
|
25
|
+
export function toPath(path: string): string[] {
|
|
26
|
+
return path.match(/[^.[\]]+/g) ?? []
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Returns `any` (not `T | undefined`) to match lodash's loose `get` contract,
|
|
30
|
+
// so existing call sites that treat the result as `any` keep type-checking.
|
|
31
|
+
export function get<T = any>(object: unknown, path: string): T {
|
|
32
|
+
if (object == null) return undefined as T
|
|
33
|
+
let current: any = object
|
|
34
|
+
for (const key of toPath(path)) {
|
|
35
|
+
if (current == null) return undefined as T
|
|
36
|
+
current = current[key]
|
|
37
|
+
}
|
|
38
|
+
return current as T
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function set<T extends object>(object: T, path: string, value: unknown): T {
|
|
42
|
+
if (object == null) return object
|
|
43
|
+
const keys = toPath(path)
|
|
44
|
+
if (keys.length === 0) return object
|
|
45
|
+
|
|
46
|
+
let current: any = object
|
|
47
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
48
|
+
// Bounded by the loop condition, so these indexed reads are always defined.
|
|
49
|
+
const key = keys[i] as string
|
|
50
|
+
const nextKey = keys[i + 1] as string
|
|
51
|
+
const existing = current[key]
|
|
52
|
+
if (existing == null || typeof existing !== 'object') {
|
|
53
|
+
// Create the container the next segment needs: array for an index, else object.
|
|
54
|
+
current[key] = isIndexKey(nextKey) ? [] : {}
|
|
55
|
+
}
|
|
56
|
+
current = current[key]
|
|
57
|
+
}
|
|
58
|
+
current[keys[keys.length - 1] as string] = value
|
|
59
|
+
return object
|
|
60
|
+
}
|