Skip to content

Commit a331620

Browse files
authored
Merge pull request #200 from PRO-Robotech/feature/dev
purge arr elements after deletion | relative path in lists
2 parents 1b75c60 + 99c32af commit a331620

File tree

7 files changed

+155
-32
lines changed

7 files changed

+155
-32
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@prorobotech/openapi-k8s-toolkit",
3-
"version": "0.0.1-alpha.143",
3+
"version": "0.0.1-alpha.144",
44
"description": "ProRobotech OpenAPI k8s tools",
55
"main": "dist/openapi-k8s-toolkit.cjs.js",
66
"module": "dist/openapi-k8s-toolkit.es.js",

src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
useIsTouchedPersisted,
1919
useUpdateIsTouchedPersisted,
2020
} from '../../organisms/BlackholeForm/context'
21+
import { resolveFormPath, normalizeNameToPath, listItemBasePath } from './utils'
2122

2223
type TFormListInputProps = {
2324
name: TFormName
@@ -61,9 +62,20 @@ export const FormListInput: FC<TFormListInputProps> = ({
6162

6263
const fixedName = name === 'nodeName' ? 'nodeNameBecauseOfSuddenBug' : name
6364

64-
const rawRelatedFieldValue = Form.useWatch(customProps.relatedValuePath, form)
65-
const relatedFieldValue = customProps.relatedValuePath ? rawRelatedFieldValue : undefined
66-
const relatedTouched = customProps.relatedValuePath ? form.isFieldTouched(customProps.relatedValuePath) : '~'
65+
// Build absolute path of this field from the full 'fixedName'
66+
const fullFieldPath = normalizeNameToPath(fixedName)
67+
68+
// Base for relative paths = the list item object (e.g. ["spec","hosts",0])
69+
const baseForRelative = listItemBasePath(fullFieldPath)
70+
71+
// Resolve the (string) related path against that base
72+
const relatedPath = customProps.relatedValuePath
73+
? resolveFormPath(customProps.relatedValuePath, baseForRelative)
74+
: undefined
75+
76+
const rawRelatedFieldValue = Form.useWatch(relatedPath, form)
77+
const relatedFieldValue = relatedPath ? rawRelatedFieldValue : undefined
78+
const relatedTouched = relatedPath ? form.isFieldTouched(relatedPath) : '~'
6779

6880
// to prevent circular callback onvaluechange call
6981
const hasFiredRef = useRef(false)
@@ -74,29 +86,24 @@ export const FormListInput: FC<TFormListInputProps> = ({
7486

7587
// managing context of touched/untouched of related field
7688
useEffect(() => {
77-
if (customProps.relatedValuePath && relatedTouched) {
89+
if (relatedPath && relatedTouched) {
7890
updateTouched(prev => ({
7991
...prev,
80-
[customProps.relatedValuePath?.join('.') || 'your doing it wrong']: true,
92+
[relatedPath.join('.') || 'your doing it wrong']: true,
8193
}))
8294
}
83-
}, [customProps.relatedValuePath, relatedTouched, updateTouched])
95+
}, [relatedPath, relatedTouched, updateTouched])
8496

8597
// updating prev value on first user touch of related field
8698
useEffect(() => {
87-
if (
88-
customProps.relatedValuePath &&
89-
isTouchedPeristed[customProps.relatedValuePath.join('.')] &&
90-
relatedFieldValue &&
91-
!hasSeededRef.current
92-
) {
99+
if (relatedPath && isTouchedPeristed[relatedPath.join('.')] && relatedFieldValue && !hasSeededRef.current) {
93100
relatedFieldValuePrev.current = relatedFieldValue
94101
hasSeededRef.current = true
95102
form.setFieldValue(arrName || fixedName, undefined)
96103
onValuesChangeCallBack?.()
97104
}
98105
}, [
99-
customProps.relatedValuePath,
106+
relatedPath,
100107
relatedTouched,
101108
isTouchedPeristed,
102109
relatedFieldValue,
@@ -108,7 +115,7 @@ export const FormListInput: FC<TFormListInputProps> = ({
108115

109116
useEffect(() => {
110117
// only if previous value is touched
111-
if (!customProps.relatedValuePath || relatedFieldValuePrev.current === 'unset') {
118+
if (!relatedPath || relatedFieldValuePrev.current === 'unset') {
112119
return
113120
}
114121

@@ -129,15 +136,7 @@ export const FormListInput: FC<TFormListInputProps> = ({
129136
// user has set it back → re‑arm for the next clear
130137
hasFiredRef.current = false
131138
}
132-
}, [
133-
customProps.relatedValuePath,
134-
form,
135-
arrName,
136-
fixedName,
137-
relatedFieldValue,
138-
onValuesChangeCallBack,
139-
isTouchedPeristed,
140-
])
139+
}, [relatedPath, form, arrName, fixedName, relatedFieldValue, onValuesChangeCallBack, isTouchedPeristed])
141140

142141
const uri = prepareTemplate({
143142
template: customProps.valueUri,
@@ -152,7 +151,7 @@ export const FormListInput: FC<TFormListInputProps> = ({
152151
uri,
153152
refetchInterval: false,
154153
queryKey: [uri || '', JSON.stringify(name)],
155-
isEnabled: !!uri && (!customProps.relatedValuePath || (customProps.relatedValuePath && !!relatedFieldValue)),
154+
isEnabled: !!uri && (!relatedPath || (relatedPath && !!relatedFieldValue)),
156155
})
157156

158157
if (isLoadingOptionsObj && (!customProps.relatedValuePath || (customProps.relatedValuePath && !!relatedFieldValue))) {
@@ -262,7 +261,7 @@ export const FormListInput: FC<TFormListInputProps> = ({
262261
placeholder="Select"
263262
options={uniqueOptions}
264263
filterOption={filterSelectOptions}
265-
disabled={customProps.relatedValuePath && !rawRelatedFieldValue}
264+
disabled={relatedPath && !rawRelatedFieldValue}
266265
allowClear
267266
showSearch
268267
/>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/* eslint-disable no-restricted-syntax */
2+
/* eslint-disable no-continue */
3+
type PathSeg = string | number
4+
5+
// Find the nearest list item base by taking the prefix up to (and including) the last numeric segment.
6+
// If none, fall back to the parent object (drop last key).
7+
export const listItemBasePath = (fullFieldPath: PathSeg[]): PathSeg[] => {
8+
for (let i = fullFieldPath.length - 1; i >= 0; i--) {
9+
if (typeof fullFieldPath[i] === 'number') {
10+
return fullFieldPath.slice(0, i + 1) // e.g. ["spec","hosts",0]
11+
}
12+
}
13+
return fullFieldPath.slice(0, -1) // no list → parent object
14+
}
15+
16+
// turn "spec.hosts.0.namespace" → ["spec","hosts",0,"namespace"]
17+
const parseDotPath = (dotPath: string): PathSeg[] =>
18+
dotPath
19+
.split('.')
20+
.filter(Boolean)
21+
.map(seg => (seg.match(/^\d+$/) ? Number(seg) : seg))
22+
23+
export const resolveFormPath = (
24+
pathInput: string | string[] | undefined,
25+
basePathForRelative: PathSeg[],
26+
): PathSeg[] => {
27+
if (!pathInput) return []
28+
29+
// if it's already an array (from old usage), just return as-is
30+
if (Array.isArray(pathInput)) return pathInput
31+
32+
const pathStr = String(pathInput)
33+
const isRelative = pathStr.startsWith('./') || pathStr.startsWith('../')
34+
35+
if (!isRelative) {
36+
// absolute (dot notation)
37+
return parseDotPath(pathStr)
38+
}
39+
40+
// relative: split by "/" then interpret each segment
41+
let resolved: PathSeg[] = [...basePathForRelative]
42+
const parts = pathStr.split('/').filter(Boolean) // e.g. ["..", "..", "foo"]
43+
44+
for (const part of parts) {
45+
if (part === '.') {
46+
continue
47+
}
48+
if (part === '..') {
49+
resolved = resolved.slice(0, -1)
50+
continue
51+
}
52+
resolved.push(...parseDotPath(part))
53+
}
54+
55+
return resolved
56+
}
57+
58+
// Normalize any antd name → array path
59+
export const normalizeNameToPath = (name: unknown): PathSeg[] =>
60+
Array.isArray(name) ? (name as PathSeg[]) : [name as string]

src/components/molecules/BlackholeForm/organisms/BlackholeForm/BlackholeForm.tsx

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-restricted-syntax */
12
/* eslint-disable no-plusplus */
23
/* eslint-disable @typescript-eslint/no-explicit-any */
34
/* eslint-disable no-nested-ternary */
@@ -36,7 +37,7 @@ import {
3637
scrubLiteralWildcardKeys,
3738
} from './helpers/prefills'
3839
import { DEBUG_PREFILLS, dbg, group, end, wdbg, wgroup, wend, prettyPath } from './helpers/debugs'
39-
import { sanitizeWildcardPath, expandWildcardTemplates, toStringPath } from './helpers/hiddenExpanded'
40+
import { sanitizeWildcardPath, expandWildcardTemplates, toStringPath, isPrefix } from './helpers/hiddenExpanded'
4041
import { handleSubmitError, handleValidationError } from './utilsErrorHandler'
4142
import { Styled } from './styled'
4243
import {
@@ -501,11 +502,11 @@ export const BlackholeForm: FC<TBlackholeFormCreateProps> = ({
501502
v,
502503
{ includeMissingExact: true }, // only hidden opts in
503504
)
504-
wdbg('hidden resolved', hiddenResolved.map(prettyPath)) // (typo? keep your original prettyPath)
505+
wdbg('hidden resolved', hiddenResolved.map(prettyPath))
505506

506507
setResolvedHiddenPaths(hiddenResolved as TFormName[])
507508

508-
const expandedResolved = expandWildcardTemplates(expandedWildcardTemplates, v) // unchanged
509+
const expandedResolved = expandWildcardTemplates(expandedWildcardTemplates, v)
509510
wdbg('expanded resolved', expandedResolved.map(prettyPath))
510511

511512
// Merge auto-expanded with current expandedKeys (preserve user choices)
@@ -533,6 +534,66 @@ export const BlackholeForm: FC<TBlackholeFormCreateProps> = ({
533534
const newLengths = collectArrayLengths(v)
534535
const prevLengths = prevArrayLengthsRef.current
535536

537+
// If you delete arr el and then add it again. There is no purge
538+
// Adding purge:
539+
// --- handle SHRINK: indices removed ---
540+
for (const [k, prevLen] of prevLengths.entries()) {
541+
const newLen = newLengths.get(k) ?? 0
542+
if (newLen < prevLen) {
543+
const arrayPath = JSON.parse(k) as (string | number)[]
544+
for (let i = newLen; i < prevLen; i++) {
545+
// purge UI state + tombstones under removed index
546+
const removedPrefix = [...arrayPath, i]
547+
548+
// drop expansions/persisted under this subtree
549+
setExpandedKeys(prev =>
550+
prev.filter(p => {
551+
const full = Array.isArray(p) ? p : [p]
552+
return !isPrefix(full, removedPrefix)
553+
}),
554+
)
555+
setPersistedKeys(prev =>
556+
prev.filter(p => {
557+
const full = Array.isArray(p) ? p : [p]
558+
return !isPrefix(full, removedPrefix)
559+
}),
560+
)
561+
562+
// clear any blocks (tombstones) beneath removed index
563+
for (const k of [...blockedPathsRef.current]) {
564+
const path = JSON.parse(k) as (string | number)[]
565+
if (isPrefix(path, removedPrefix)) blockedPathsRef.current.delete(k)
566+
}
567+
}
568+
}
569+
}
570+
571+
// --- handle GROW: indices added ---
572+
for (const [k, newLen] of newLengths.entries()) {
573+
const prevLen = prevLengths.get(k) ?? 0
574+
if (newLen > prevLen) {
575+
const arrayPath = JSON.parse(k) as (string | number)[]
576+
for (let i = prevLen; i < newLen; i++) {
577+
const itemPath = [...arrayPath, i]
578+
579+
// ensure the node exists to stabilize render/hidden resolution
580+
const itemVal = form.getFieldValue(itemPath as any)
581+
if (typeof itemVal === 'undefined') {
582+
form.setFieldValue(itemPath as any, {}) // for object arrays; harmless for others
583+
}
584+
585+
// guarantee no stale tombstone blocks this subtree
586+
blockedPathsRef.current.delete(JSON.stringify(itemPath))
587+
588+
// your existing prefills (wildcards etc.)
589+
applyPrefillForNewArrayItem(arrayPath, i)
590+
}
591+
}
592+
}
593+
594+
// keep tracker in sync
595+
prevArrayLengthsRef.current = newLengths
596+
536597
// dump lengths (readable)
537598
dbg('array lengths (prev → new)')
538599
const allKeys = new Set([...prevLengths.keys(), ...newLengths.keys()])

src/components/molecules/BlackholeForm/organisms/BlackholeForm/helpers/hiddenExpanded.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,6 @@ const isPathArray = (p: TFormName): p is (string | number)[] => Array.isArray(p)
133133

134134
export const toStringPath = (p: TFormName): string[] =>
135135
isPathArray(p) ? (p as (string | number)[]).map(String) : [String(p)]
136+
137+
export const isPrefix = (full: (string | number)[], prefix: (string | number)[]) =>
138+
prefix.length <= full.length && prefix.every((seg, i) => full[i] === seg)

src/localTypes/formExtensions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,5 @@ export type TListInputCustomProps = {
5656
value: unknown
5757
keepPrefilled?: boolean
5858
}
59-
relatedValuePath?: string[]
59+
relatedValuePath?: string
6060
}

0 commit comments

Comments
 (0)