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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,24 @@
import { useCallback, useRef, useEffect, useMemo } from 'react'
import { debounce, isNil, isEmpty } from 'lodash'
import { uuid } from '@Pimcore/utils/uuid'
import { container } from '@Pimcore/app/depency-injection'
import { useInjection } from '@Pimcore/app/depency-injection'
import { serviceIds } from '@Pimcore/app/config/services/service-ids'
import { type DebouncedFormRegistry } from '../services/debounced-form-registry'
import { useDebouncedFormContext } from '../providers/debounced-form-provider'

export interface UseDebouncedFormChangeOptions {
/**
* If true, debouncing is disabled and the original callback is returned unchanged.
*/
disabled?: boolean
delay?: number
/**
* Field names that bypass debouncing and fire onChange immediately.
*/
immediateFields?: string[]
/**
* Tag for registry coordination. Gets auto-resolved from DebouncedFormProvider if omitted.
*/
tag?: string
}

Expand All @@ -30,9 +41,11 @@ export const useDebouncedFormChange = (
onFormChange: (changedValues: Record<string, any>, allValues: Record<string, any>) => void,
options: UseDebouncedFormChangeOptions = {}
): UseDebouncedFormChangeReturn => {
const { delay = 300, immediateFields = [], tag } = options
const { disabled = false, delay = 300, immediateFields = [] } = options
const resolvedTag = useDebouncedFormContext(options.tag)
const registry = useInjection<DebouncedFormRegistry>(serviceIds.debouncedFormRegistry)

const registryKey = useMemo(() => `${tag ?? 'default'}-${uuid()}`, [tag])
const registryKey = useMemo(() => `${resolvedTag ?? 'default'}-${uuid()}`, [resolvedTag])

const debouncedChangeRef = useRef(
debounce((changedValues: Record<string, any>, allValues: Record<string, any>) => {
Expand All @@ -41,6 +54,11 @@ export const useDebouncedFormChange = (
)

const handleFormChange = useCallback((changedValues: Record<string, any>, allValues: Record<string, any>) => {
if (disabled) {
onFormChange(changedValues, allValues)
return
}

const immediateChanges: Record<string, any> = {}
const debouncedChanges: Record<string, any> = {}

Expand All @@ -66,14 +84,13 @@ export const useDebouncedFormChange = (
}, [])

useEffect(() => {
if (!isNil(tag) && !isEmpty(tag)) {
const registry = container.get<DebouncedFormRegistry>(serviceIds.debouncedFormRegistry)
registry.register(registryKey, flush, tag)
if (!isNil(resolvedTag) && !isEmpty(resolvedTag)) {
registry.register(registryKey, flush, resolvedTag)
return () => {
registry.unregister(registryKey)
}
}
}, [registryKey, flush, tag])
}, [registry, registryKey, flush, resolvedTag])

return {
handleFormChange,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

import { useCallback } from 'react'
import { useInjection } from '@Pimcore/app/depency-injection'
import { serviceIds } from '@Pimcore/app/config/services/service-ids'
import { type DebouncedFormRegistry } from '../services/debounced-form-registry'
import { useDebouncedFormContext } from '../providers/debounced-form-provider'

/**
* Returns a function to flush all debounced forms with the given tag.
* Gets tag from explicit parameter or DebouncedFormProvider context.
* Throws if no tag is available from either source.
*/
export function useDebouncedFormFlush (tag?: string): () => void {
const resolvedTag = useDebouncedFormContext(tag)

if (resolvedTag === undefined) {
throw new Error(
'useDebouncedFormFlush: No tag provided. Either pass a tag as argument or render inside a DebouncedFormProvider.'
)
}

const registry = useInjection<DebouncedFormRegistry>(serviceIds.debouncedFormRegistry)

return useCallback(() => {
registry.flushByTag(resolvedTag)
}, [registry, resolvedTag])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

import React, { createContext, useContext, useMemo, type ReactNode } from 'react'
import { uuid } from '@Pimcore/utils/uuid'

export interface DebouncedFormContextValue {
tag: string
}

const DebouncedFormContext = createContext<DebouncedFormContextValue | null>(null)

export interface DebouncedFormProviderProps {
/**
* Tag for coordinating flushes across multiple forms. Auto-generated if not provided.
*/
tag?: string
children: ReactNode
}

/**
* Provider for coordinating debounced form changes.
* Forms inside this provider can be flushed together via `useDebouncedFormFlush()`.
*/
export function DebouncedFormProvider ({
tag: providedTag,
children
}: DebouncedFormProviderProps): React.JSX.Element {
const tag = useMemo(() => providedTag ?? `debounced-form-${uuid()}`, [providedTag])

const contextValue = useMemo<DebouncedFormContextValue>(() => ({
tag
}), [tag])

return (
<DebouncedFormContext.Provider value={ contextValue }>
{children}
</DebouncedFormContext.Provider>
)
}

/**
* Returns the resolved tag from explicit parameter or provider context.
*
* Priority: explicitTag → provider tag → undefined
*/
export function useDebouncedFormContext (explicitTag?: string): string | undefined {
const context = useContext(DebouncedFormContext)
return explicitTag ?? context?.tag
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,51 +8,55 @@
* @license Pimcore Open Core License (POCL)
*/

import React, { type ReactNode } from 'react'
import React, { type ReactNode, useCallback, useMemo } from 'react'
import { Form, FormKit } from '@sdk/components'
import { useDebouncedFormChange, type UseDebouncedFormChangeOptions } from '@Pimcore/components/form/hooks/use-debounced-form-change'

interface RuleConfigFormProps<T extends Record<string, any>> {
config: T
onChange: (config: T) => void
disabled?: boolean
children: ReactNode
/**
* Debounce options for forms with text inputs or continuously-changing fields.
* - delay: Debounce delay in ms (default: 300)
* - immediateFields: Fields that bypass debouncing
* - tag: Tag for flush coordination (auto-resolved from provider if omitted)
*/
debounceOptions?: UseDebouncedFormChangeOptions
}

/**
* Reusable form wrapper for rule configurations (conditions, actions, triggers).
* Eliminates boilerplate for form setup, change tracking, and value synchronization.
*
* @example
* ```tsx
* <RuleConfigForm config={config} onChange={onChange} disabled={disabled}>
* <Form.Item label={t('label')} name="field">
* <Input />
* </Form.Item>
* </RuleConfigForm>
* ```
*/
export function RuleConfigForm<T extends Record<string, any>> ({
config,
onChange,
disabled,
children
children,
debounceOptions
}: RuleConfigFormProps<T>): React.JSX.Element {
const [form] = Form.useForm<T>()

const handleValuesChange = (_changedValues: Partial<T>, allValues: T): void => {
const handleValuesChange = useCallback((_changedValues: Partial<T>, allValues: T): void => {
onChange(allValues)
}
}, [onChange])

const { handleFormChange } = useDebouncedFormChange(handleValuesChange, {
...debounceOptions,
disabled: debounceOptions === undefined
})

const formProps = useMemo(() => ({
form,
component: false as const,
disabled,
initialValues: config,
onValuesChange: handleFormChange
}), [form, disabled, config, handleFormChange])

return (
<FormKit
formProps={ {
form,
component: false,
initialValues: config,
onValuesChange: handleValuesChange,
disabled
} }
>
<FormKit formProps={ formProps }>
{children}
</FormKit>
)
Expand Down
3 changes: 3 additions & 0 deletions assets/js/src/sdk/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ export * from '@Pimcore/components/focal-point/focal-point'
export * from '@Pimcore/components/focal-point/provider/focal-point-provider'
export * from '@Pimcore/components/form/form'
export * from '@Pimcore/components/form/form-kit'
export * from '@Pimcore/components/form/providers/debounced-form-provider'
export * from '@Pimcore/components/form/services/debounced-form-registry'
export * from '@Pimcore/components/form/hooks/use-debounced-form-flush'
export * from '@Pimcore/components/formatted-date/formatted-date'
export * from '@Pimcore/components/formatted-date-time/formatted-date-time'
export * from '@Pimcore/components/formatted-time/formatted-time'
Expand Down

This file was deleted.

24 changes: 0 additions & 24 deletions public/build/22582db6-7807-41c8-949c-36eb5a7af70d/entrypoints.json

This file was deleted.

35 changes: 0 additions & 35 deletions public/build/22582db6-7807-41c8-949c-36eb5a7af70d/manifest.json

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions public/build/6ce2e5ff-49c2-429a-9d8c-3b95587f8947/entrypoints.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions public/build/6ce2e5ff-49c2-429a-9d8c-3b95587f8947/manifest.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading