From e554b64cc4685015157d9643d41ca353cba5a66b Mon Sep 17 00:00:00 2001 From: typescreep Date: Mon, 29 Sep 2025 14:04:23 +0300 Subject: [PATCH] preserver all fields --- src/components/organisms/Search/Search.tsx | 128 +++++++++++-------- src/components/organisms/Search/constants.ts | 5 +- src/components/organisms/Search/utils.ts | 17 +++ 3 files changed, 95 insertions(+), 55 deletions(-) diff --git a/src/components/organisms/Search/Search.tsx b/src/components/organisms/Search/Search.tsx index 23fd9f7..863dbd3 100644 --- a/src/components/organisms/Search/Search.tsx +++ b/src/components/organisms/Search/Search.tsx @@ -1,5 +1,5 @@ /* eslint-disable max-lines-per-function */ -import React, { FC, Fragment, useState, useEffect } from 'react' +import React, { FC, Fragment, useState, useEffect, useLayoutEffect, useRef } from 'react' import { useLocation, useSearchParams } from 'react-router-dom' import { Search as PackageSearch, @@ -9,7 +9,6 @@ import { TKindWithVersion, getKinds, getSortedKinds, - // kindByGvr, } from '@prorobotech/openapi-k8s-toolkit' import { ConfigProvider, theme as antdtheme, Form, Spin, Alert } from 'antd' import { useSelector } from 'react-redux' @@ -20,13 +19,22 @@ import { FIELD_NAME_STRING, FIELD_NAME_LABELS, FIELD_NAME_FIELDS, - TYPE_SELECTOR, QUERY_KEY, NAME_QUERY_KEY, LABELS_QUERY_KEY, FIELDS_QUERY_KEY, + TYPE_SELECTOR, + TYPE_QUERY_KEY, } from './constants' -import { useDebouncedCallback, getArrayParam, setArrayParam, getStringParam, setStringParam } from './utils' +import { + useDebouncedCallback, + getArrayParam, + setArrayParam, + getStringParam, + setStringParam, + getTypeParam, + setTypeParam, +} from './utils' import { SearchEntry } from './molecules' import { Styled } from './styled' @@ -67,9 +75,7 @@ export const Search: FC = () => { useEffect(() => { setIsLoading(true) setError(undefined) - getKinds({ - clusterName: cluster, - }) + getKinds({ clusterName: cluster }) .then(data => { setKindIndex(data) setKindWithVersion(getSortedKinds(data)) @@ -88,59 +94,79 @@ export const Search: FC = () => { const watchedFields = Form.useWatch(FIELD_NAME_FIELDS, form) const watchedTypedSelector = Form.useWatch(TYPE_SELECTOR, form) - // Apply current values from search params on mount / when URL changes + // —— hydration control to prevent “push empties” on first render —— + const isHydratingRef = useRef(true) + + // First, synchronously hydrate form from URL so watchers aren’t undefined on paint + useLayoutEffect(() => { + const fromKinds = getArrayParam(searchParams, QUERY_KEY) + const fromName = getStringParam(searchParams, NAME_QUERY_KEY) + const fromLabels = getArrayParam(searchParams, LABELS_QUERY_KEY) + const fromFields = getArrayParam(searchParams, FIELDS_QUERY_KEY) + + const explicitType = getTypeParam(searchParams, TYPE_QUERY_KEY) + const inferredFromValues = + (fromFields.length > 0 && 'fields') || (fromLabels.length > 0 && 'labels') || (fromName ? 'name' : undefined) + const nextType = explicitType ?? inferredFromValues + + form.setFieldsValue({ + [FIELD_NAME]: fromKinds, + [FIELD_NAME_STRING]: fromName, + [FIELD_NAME_LABELS]: fromLabels, + [FIELD_NAME_FIELDS]: fromFields, + [TYPE_SELECTOR]: nextType, + }) + + isHydratingRef.current = false + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // keep form in sync if URL changes later (back/forward/external edits) —— useEffect(() => { + if (isHydratingRef.current) return + const fromKinds = getArrayParam(searchParams, QUERY_KEY) const currentKinds = form.getFieldValue(FIELD_NAME) const kindsDiffer = (fromKinds.length || 0) !== (currentKinds?.length || 0) || fromKinds.some((v, i) => v !== currentKinds?.[i]) - // name const fromName = getStringParam(searchParams, NAME_QUERY_KEY) - const currentName = form.getFieldValue(FIELD_NAME_STRING) as string | undefined + const currentName = form.getFieldValue(FIELD_NAME_STRING) const nameDiffer = (fromName || '') !== (currentName || '') - // labels const fromLabels = getArrayParam(searchParams, LABELS_QUERY_KEY) - const currentLabels = form.getFieldValue(FIELD_NAME_LABELS) as string[] | undefined + const currentLabels = form.getFieldValue(FIELD_NAME_LABELS) const labelsDiffer = (fromLabels.length || 0) !== (currentLabels?.length || 0) || fromLabels.some((v, i) => v !== currentLabels?.[i]) - // labels const fromFields = getArrayParam(searchParams, FIELDS_QUERY_KEY) - const currentFields = form.getFieldValue(FIELD_NAME_FIELDS) as string[] | undefined + const currentFields = form.getFieldValue(FIELD_NAME_FIELDS) const fieldsDiffer = (fromFields.length || 0) !== (currentFields?.length || 0) || fromFields.some((v, i) => v !== currentFields?.[i]) - // decide type from params + const explicitType = getTypeParam(searchParams, TYPE_QUERY_KEY) const currentType = form.getFieldValue(TYPE_SELECTOR) - let inferredType: string | undefined - if (fromName) { - inferredType = 'name' - } else if (fromLabels.length > 0) { - inferredType = 'labels' - } else if (fromFields.length > 0) { - inferredType = 'fields' - } - const typeDiffer = inferredType !== currentType + const inferredFromValues = + (fromFields.length > 0 && 'fields') || (fromLabels.length > 0 && 'labels') || (fromName ? 'name' : undefined) + const nextType = explicitType ?? currentType ?? inferredFromValues + const typeDiffer = nextType !== currentType - // Only update the form if URL differs from form (prevents loops) - if (kindsDiffer || nameDiffer || labelsDiffer || fieldsDiffer) { + if (kindsDiffer || nameDiffer || labelsDiffer || fieldsDiffer || typeDiffer) { form.setFieldsValue({ [FIELD_NAME]: kindsDiffer ? fromKinds : currentKinds, [FIELD_NAME_STRING]: nameDiffer ? fromName : currentName, [FIELD_NAME_LABELS]: labelsDiffer ? fromLabels : currentLabels, [FIELD_NAME_FIELDS]: fieldsDiffer ? fromFields : currentFields, - [TYPE_SELECTOR]: typeDiffer ? inferredType : currentType, + ...(typeDiffer ? { [TYPE_SELECTOR]: nextType } : {}), }) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [location.search]) // react to back/forward, external URL edits + }, [location.search]) - // Watch field changes to push to URL (debounced) - const debouncedPush = useDebouncedCallback((values: string[]) => { + // debounced URL pushers (guarded against hydration & undefined) —— + const debouncedPushKinds = useDebouncedCallback((values: string[]) => { const next = setArrayParam(searchParams, QUERY_KEY, values) - setSearchParams(next, { replace: true }) // replace to keep history cleaner + setSearchParams(next, { replace: true }) }, 250) const debouncedPushName = useDebouncedCallback((value: string) => { @@ -159,47 +185,43 @@ export const Search: FC = () => { }, 250) useEffect(() => { - debouncedPush(watchedKinds || []) + if (isHydratingRef.current || watchedKinds === undefined) { + return + } + debouncedPushKinds(watchedKinds) // eslint-disable-next-line react-hooks/exhaustive-deps }, [watchedKinds]) useEffect(() => { - debouncedPushName((watchedName || '').trim()) + if (isHydratingRef.current || watchedName === undefined) { + return + } + debouncedPushName(watchedName.trim()) // eslint-disable-next-line react-hooks/exhaustive-deps }, [watchedName]) useEffect(() => { + if (isHydratingRef.current || watchedLabels === undefined) { + return + } debouncedPushLabels(watchedLabels || []) // eslint-disable-next-line react-hooks/exhaustive-deps }, [watchedLabels]) useEffect(() => { + if (isHydratingRef.current || watchedFields === undefined) { + return + } debouncedPushFields(watchedFields || []) // eslint-disable-next-line react-hooks/exhaustive-deps }, [watchedFields]) useEffect(() => { - if (watchedTypedSelector === 'name') { - // Clear labels when switching to "name" - // const cur = form.getFieldValue(FIELD_NAME_LABELS) as string[] | undefined - // if (cur?.length) { - form.setFieldsValue({ [FIELD_NAME_LABELS]: [], [FIELD_NAME_FIELDS]: [] }) - // } - } else if (watchedTypedSelector === 'labels') { - // Clear name when switching to "labels" - // const cur = (form.getFieldValue(FIELD_NAME_STRING) as string | undefined) ?? '' - // if (cur) { - form.setFieldsValue({ [FIELD_NAME_STRING]: '', [FIELD_NAME_FIELDS]: [] }) - // } - } else if (watchedTypedSelector === 'fields') { - // Clear name when switching to "labels" - // const cur = (form.getFieldValue(FIELD_NAME_STRING) as string | undefined) ?? '' - // if (cur) { - form.setFieldsValue({ [FIELD_NAME_STRING]: '', [FIELD_NAME_LABELS]: [] }) - // } + if (isHydratingRef.current || watchedTypedSelector === undefined) { + return } - // Optional: if undefined (e.g., initial), choose a default behavior: - // else { form.setFieldsValue({ [FIELD_NAME_STRING]: '', [FIELD_NAME_MULTIPLE]: [] }) } + const next = setTypeParam(searchParams, TYPE_QUERY_KEY, watchedTypedSelector) + setSearchParams(next, { replace: true }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [watchedTypedSelector]) diff --git a/src/components/organisms/Search/constants.ts b/src/components/organisms/Search/constants.ts index 021c83e..af6cc45 100644 --- a/src/components/organisms/Search/constants.ts +++ b/src/components/organisms/Search/constants.ts @@ -3,9 +3,10 @@ export const FIELD_NAME_STRING = 'name' export const FIELD_NAME_LABELS = 'labels' export const FIELD_NAME_FIELDS = 'fields' -export const TYPE_SELECTOR = 'TYPE_SELECTOR' - export const QUERY_KEY = 'kinds' // the query param name export const NAME_QUERY_KEY = 'name' export const LABELS_QUERY_KEY = 'labels' export const FIELDS_QUERY_KEY = 'fields' + +export const TYPE_SELECTOR = 'TYPE_SELECTOR' +export const TYPE_QUERY_KEY = 'type' diff --git a/src/components/organisms/Search/utils.ts b/src/components/organisms/Search/utils.ts index bcc977f..01d2034 100644 --- a/src/components/organisms/Search/utils.ts +++ b/src/components/organisms/Search/utils.ts @@ -40,3 +40,20 @@ export const setStringParam = (sp: URLSearchParams, key: string, value: string | else next.set(key, v) return next } + +export const getTypeParam = (sp: URLSearchParams, key: string): 'name' | 'labels' | 'fields' | undefined => { + const v = sp.get(key)?.trim() + if (v === 'name' || v === 'labels' || v === 'fields') return v + return undefined +} + +export const setTypeParam = (sp: URLSearchParams, key: string, value: string | undefined | null) => { + const next = new URLSearchParams(sp) + const v = (value ?? '').trim() + if (v !== 'name' && v !== 'labels' && v !== 'fields') { + next.delete(key) + } else { + next.set(key, v) + } + return next +}