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
  •  
  •  
  •  
45 changes: 43 additions & 2 deletions assets/js/src/core/components/form/conditional/conditional.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,37 @@
* @license Pimcore Open Core License (POCL)
*/

import React, { useMemo } from 'react'
import { isUndefined } from 'lodash'
import React, { useCallback, useMemo } from 'react'
import { Form } from '../form'

export interface ConditionalProps {
/** Evaluate whether children should be rendered based on current form values */
condition: (formValues: Record<string, unknown>) => boolean
children: React.ReactNode
/** Optional: specify field names to watch. When provided, only changes to these
* fields trigger re-renders instead of subscribing to all form value changes. */
watchFields?: string[]
}

export const Conditional = ({ condition, children }: ConditionalProps): React.JSX.Element => {
const ConditionalWithWatchFields = ({ condition, children, watchFields }: ConditionalProps & { watchFields: string[] }): React.JSX.Element => {
const form = Form.useFormInstance()

const selector = useCallback((values: Record<string, unknown>) => {
return watchFields.map((field) => values[field])
}, [watchFields])

const watchedValues = Form.useWatch(selector, form)

const isTrue = useMemo(() => {
const values = form.getFieldsValue(true) as Record<string, unknown>
return condition(values)
}, [condition, watchedValues])

return isTrue ? <>{children}</> : <></>
}

const ConditionalWithAllValues = ({ condition, children }: Omit<ConditionalProps, 'watchFields'>): React.JSX.Element => {
const initialValues = Form.useFormInstance().getFieldsValue(true)
const values = Form.useWatch((values) => {
return values
Expand All @@ -28,3 +50,22 @@ export const Conditional = ({ condition, children }: ConditionalProps): React.JS

return isTrue ? <>{children}</> : <></>
}

export const Conditional = ({ condition, children, watchFields }: ConditionalProps): React.JSX.Element => {
if (!isUndefined(watchFields)) {
return (
<ConditionalWithWatchFields
condition={ condition }
watchFields={ watchFields }
>
{children}
</ConditionalWithWatchFields>
)
}

return (
<ConditionalWithAllValues condition={ condition }>
{children}
</ConditionalWithAllValues>
)
}
24 changes: 11 additions & 13 deletions assets/js/src/core/components/form/use-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import { Form, type FormInstance, type FormProps } from 'antd'
import { type NamePath } from 'antd/es/form/interface'
import { set } from 'lodash'
import React, { createContext, useMemo } from 'react'
import React, { createContext, useCallback, useMemo } from 'react'
import type { VirtualValidatorRegistry } from './item/hooks/use-virtual-validator-registry'

export interface CustomSetFieldValueOptions {
Expand All @@ -29,41 +29,39 @@ export type formInstanceType<Values = any> = Omit<FormInstance<Values>, 'setFiel

export const useForm = <Values = any>(form?: FormInstance<Values>): [formInstanceType<Values>] => {
const [formInstance] = Form.useForm<Values>(form) as [formInstanceType<Values>]
const originalSetFieldValue = formInstance.setFieldValue
const originalSetFieldsValue = formInstance.setFieldsValue

const setOnValuesChangeHandler = (handler: FormProps<Values>['onValuesChange']): void => {
const setOnValuesChangeHandler = useCallback((handler: FormProps<Values>['onValuesChange']): void => {
formInstance._onValuesChangeHandler = handler
}
}, [formInstance])

const setFieldValue = (name: NamePath<Values>, value: any, options?: CustomSetFieldValueOptions): void => {
const setFieldValue = useCallback((name: NamePath<Values>, value: any, options?: CustomSetFieldValueOptions): void => {
const { triggerChange = false } = options ?? {}

originalSetFieldValue(name, value)
formInstance.setFieldValue(name, value)

if (triggerChange && formInstance._onValuesChangeHandler !== undefined) {
const update = {}
set(update, name as string, value)
formInstance._onValuesChangeHandler(update as Partial<Values>, formInstance.getFieldsValue())
}
}
}, [formInstance])

const setFieldsValue = (values: Partial<Values>, options?: CustomSetFieldValueOptions): void => {
const setFieldsValue = useCallback((values: Partial<Values>, options?: CustomSetFieldValueOptions): void => {
const { triggerChange = false } = options ?? {}

originalSetFieldsValue(values)
formInstance.setFieldsValue(values)

if (triggerChange && formInstance._onValuesChangeHandler !== undefined) {
formInstance._onValuesChangeHandler(values, formInstance.getFieldsValue())
}
}
}, [formInstance])

const newFormInstance: formInstanceType<Values> = {
const newFormInstance = useMemo((): formInstanceType<Values> => ({
...formInstance,
setOnValuesChangeHandler,
setFieldValue,
setFieldsValue
}
}), [formInstance, setOnValuesChangeHandler, setFieldValue, setFieldsValue])

return [newFormInstance]
}
Expand Down
7 changes: 7 additions & 0 deletions assets/js/src/core/components/tabs/tabs.styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ export const useStyles = createStyles(({ token, css }) => {
height: 100%;
}
}
`,
middleClickButton: css`
border: none;
background: none;
padding: 0;
font: inherit;
cursor: inherit;
`
}
}, { hashPriority: 'high' })
34 changes: 19 additions & 15 deletions assets/js/src/core/components/tabs/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
* @license Pimcore Open Core License (POCL)
*/

import React, { type RefObject } from 'react'
import React, { type RefObject, useCallback, useMemo } from 'react'
import { Tabs as AntdTabs, type TabsProps } from 'antd'
import { useStyles } from '@Pimcore/components/tabs/tabs.styles'
import cn from 'classnames'
import { isUndefined } from 'lodash'

export interface ITabsProps extends TabsProps {
onClose?: (any) => void
Expand All @@ -38,42 +39,45 @@ const Component = ({ items, className, activeKey, onClose, hasStickyHeader = fal
}
)

const onEdit = (key: string | React.MouseEvent<HTMLElement>, action: 'add' | 'remove'): void => {
const onEdit = useCallback((key: string | React.MouseEvent<HTMLElement>, action: 'add' | 'remove'): void => {
if (action === 'remove' && onClose !== undefined) {
onClose(key)
}
}
}, [onClose])

// Check if any tabs are explicitly closable to determine the tab type
const hasClosableTabs = items?.some(item => item.closable !== false) ?? false
const tabType = onClose !== undefined && hasClosableTabs ? 'editable-card' : 'line'

const handleMiddleClick = (tabKey: string) => (event: React.MouseEvent): void => {
const handleMiddleClick = useCallback((event: React.MouseEvent<HTMLElement>): void => {
if (event.button === 1 && onClose !== undefined) {
// Check if this specific tab item is closable
const tabItem = items?.find(item => item.key === tabKey)
const isTabClosable = tabItem?.closable !== false
const tabKey = event.currentTarget.dataset.tabKey
if (!isUndefined(tabKey)) {
const tabItem = items?.find(item => item.key === tabKey)
const isTabClosable = tabItem?.closable !== false

if (isTabClosable) {
event.preventDefault()
onClose(tabKey)
if (isTabClosable) {
event.preventDefault()
onClose(tabKey)
}
}
}
}
}, [onClose, items])

// Add mouse down handler to each tab item
const enhancedItems = items?.map(item => ({
const enhancedItems = useMemo(() => items?.map(item => ({
...item,
label: (
<button
onMouseDown={ handleMiddleClick(item.key) }
style={ { border: 'none', background: 'none', padding: 0, font: 'inherit', cursor: 'inherit' } }
className={ styles.middleClickButton }
data-tab-key={ item.key }
onMouseDown={ handleMiddleClick }
type="button"
>
{item.label}
</button>
)
}))
})), [items, handleMiddleClick])

return (
<AntdTabs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ export const ChartSettings = ({ currentData }: IReportConfigurationSectionProps)
/>
</Form.Item>

<Conditional condition={ (formValues) => formValues.chartType === CHART_TYPE_PIE }>
<Conditional
condition={ (formValues) => formValues.chartType === CHART_TYPE_PIE }
watchFields={ ['chartType'] }
>
<FormKit.Panel
border
contentPadding={ { top: 'none', right: 'small', bottom: 'small', left: 'small' } }
Expand All @@ -74,7 +77,10 @@ export const ChartSettings = ({ currentData }: IReportConfigurationSectionProps)
{renderSelectItem({ label: t('reports.editor.chart-settings.pie-data'), name: 'pieColumn' })}
</FormKit.Panel>
</Conditional>
<Conditional condition={ (formValues) => formValues.chartType === CHART_TYPE_LINE }>
<Conditional
condition={ (formValues) => formValues.chartType === CHART_TYPE_LINE }
watchFields={ ['chartType'] }
>
<FormKit.Panel
border
theme="fieldset"
Expand All @@ -84,7 +90,10 @@ export const ChartSettings = ({ currentData }: IReportConfigurationSectionProps)
{renderSelectItem({ label: t('reports.editor.chart-settings.y-axis'), name: 'yAxis', mode: 'multiple' })}
</FormKit.Panel>
</Conditional>
<Conditional condition={ (formValues) => formValues.chartType === CHART_TYPE_BAR }>
<Conditional
condition={ (formValues) => formValues.chartType === CHART_TYPE_BAR }
watchFields={ ['chartType'] }
>
<FormKit.Panel
border
theme="fieldset"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@

import { Content, ContentLayout, Icon, IconButton, IconTextButton, SearchInput, Toolbar, type TreeDataItem, TreeElement } from '@sdk/components'
import { isNil, isString, isUndefined } from 'lodash'
import React, { useEffect, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useWidgetEditorContext } from '../../context/hooks/use-widget-editor-context'
import { useWidgetEditorActions } from '../../context/hooks/use-widget-editor-context'
import { usePerspectiveWidgetGetConfigCollectionQuery } from '@sdk/api/perspectives'
import { type WidgetConfig } from '@Pimcore/modules/perspectives/perspectives-slice.enhanced'

export const TreeContainer = (): React.JSX.Element => {
const { t } = useTranslation()
const [searchTerm, setSearchTerm] = useState<string>('')
const [treeDataFiltered, setTreeDataFiltered] = useState<TreeDataItem[]>([])
const { openWidget, createWidget } = useWidgetEditorContext()
const { openWidget, createWidget } = useWidgetEditorActions()
const { data: widgets, isFetching, isLoading, refetch } = usePerspectiveWidgetGetConfigCollectionQuery({ skipWrapperWidgets: true })

const generateTreeStructure = (widgets: WidgetConfig[]): TreeDataItem[] => {
return [...widgets]
const generateTreeStructure = useCallback((items: WidgetConfig[]): TreeDataItem[] => {
return [...items]
.sort((a, b) => a.name.localeCompare(b.name))
.map((item: WidgetConfig) => ({
title: item.name,
Expand All @@ -34,7 +34,7 @@ export const TreeContainer = (): React.JSX.Element => {
value={ item.icon.value }
/>
}))
}
}, [])

useEffect(() => {
if (isUndefined(widgets)) {
Expand All @@ -44,9 +44,9 @@ export const TreeContainer = (): React.JSX.Element => {
if (!isUndefined(widgets)) {
setTreeDataFiltered(generateTreeStructure(widgets.items))
}
}, [widgets])
}, [widgets, generateTreeStructure])

const handleSearch = (value: string): void => {
const handleSearch = useCallback((value: string): void => {
if (value.length === 0) {
if (!isUndefined(widgets)) {
setTreeDataFiltered(generateTreeStructure(widgets.items))
Expand All @@ -65,60 +65,70 @@ export const TreeContainer = (): React.JSX.Element => {

setTreeDataFiltered(generateTreeStructure(filteredData))
}
}
}, [widgets, generateTreeStructure])

const clearSearch = (): void => {
const clearSearch = useCallback((): void => {
setSearchTerm('')
if (!isUndefined(widgets)) {
setTreeDataFiltered(generateTreeStructure(widgets.items))
}
}
}, [widgets, generateTreeStructure])

const handleRefresh = useCallback(async (): Promise<void> => {
await refetch()
}, [refetch])

const handleSelected = useCallback((key: React.Key): void => {
const widget = widgets?.items.find((w) => isString(w.id) && isString(key) && w.id === key)

if (widget !== undefined) {
void openWidget(widget.id, widget.widgetType)
}
}, [widgets, openWidget])

const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>): void => {
setSearchTerm(e.target.value)
}, [])

const toolbar = useMemo(() => (
<Toolbar justify="space-between">
<IconButton
data-testid="widget-tree-refresh-button"
icon={ { value: 'refresh' } }
loading={ isLoading || isFetching }
onClick={ handleRefresh }
title={ t('refresh') }
/>

<IconTextButton
data-testid="widget-tree-create-button"
icon={ { value: 'new' } }
loading={ isLoading || isFetching }
onClick={ createWidget }
>
{t('toolbar.new')}
</IconTextButton>
</Toolbar>
), [isLoading, isFetching, handleRefresh, createWidget, t])

return (
<ContentLayout
renderToolbar={ (
<Toolbar justify="space-between">
<IconButton
data-testid="widget-tree-refresh-button"
icon={ { value: 'refresh' } }
loading={ isLoading || isFetching }
onClick={ async () => {
await refetch()
} }
title={ t('refresh') }
/>

<IconTextButton
data-testid="widget-tree-create-button"
icon={ { value: 'new' } }
loading={ isLoading || isFetching }
onClick={ createWidget }
>
{t('toolbar.new')}
</IconTextButton>
</Toolbar>
) }
renderToolbar={ toolbar }
>
<Content
loading={ isLoading || isFetching }
padded
>
<SearchInput
onChange={ (e) => { setSearchTerm(e.target.value) } }
onChange={ handleSearchChange }
onClear={ clearSearch }
onSearch={ handleSearch }
value={ searchTerm }
withoutAddon
/>
<TreeElement
hasRoot={ false }
onSelected={ (key) => {
const widget = widgets!.items.find((w) => isString(w.id) && isString(key) && w.id === key)

if (widget !== undefined) {
void openWidget(widget.id, widget.widgetType)
}
} }
onSelected={ handleSelected }
treeData={ treeDataFiltered }
/>
</Content>
Expand Down
Loading
Loading