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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions assets/js/src/examples/listings/components/custom-listing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { container } from '@pimcore/studio-ui-bundle'
import { BaseListing, DataObjectProvider, listingDefaultProps, type ObjectListingBuilder } from '@pimcore/studio-ui-bundle/modules/data-object'
import React from 'react'

export const CustomListing = (): React.JSX.Element => {
const listingBuilder = container.get<ObjectListingBuilder>('Cars/Listing/Builder')

return (
<DataObjectProvider id={ 1 }>
<BaseListing {
...listingBuilder.build({
props: {
...listingDefaultProps,
toolbarSlotName: 'carsListing.toolbar'
},
// optional config to control behavior of the different decorators
config: {
tagFilter: {
enabled: false
}
}
})
}
/>
</DataObjectProvider>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { type AbstractDecorator } from '@pimcore/studio-ui-bundle/modules/element'
import { withHeader } from './with-header'

export const HeaderDecorator: AbstractDecorator = (props) => {
const { ViewComponent } = props

return {
...props,
ViewComponent: withHeader(ViewComponent)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ContentLayout, Header, Toolbar } from '@pimcore/studio-ui-bundle/components'
import { type AbstractDecoratorProps } from '@pimcore/studio-ui-bundle/modules/element'
import React from 'react'

export const withHeader = (BaseComponent: AbstractDecoratorProps['ViewComponent']): AbstractDecoratorProps['ViewComponent'] => {
const HeaderComponent: AbstractDecoratorProps['ViewComponent'] = (props) => {
return (
<ContentLayout renderTopBar={
<Toolbar
position="top"
theme="secondary"
>
<Header title="Cars" />
</Toolbar>
}
>
<BaseComponent />
</ContentLayout>
)
}

return HeaderComponent
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { type AbstractDecorator } from '@pimcore/studio-ui-bundle/modules/element'
import { withPqlFilterProvider } from './with-pql-filter-provider'
import { withPqlFilterSidebar } from './with-pql-filter-sidebar'
import { withPqlFilterQuery } from './with-pql-filter-query'

export const PqlFilterDecorator: AbstractDecorator = (props) => {
const { useSidebarOptions, ContextComponent, useDataQueryHelper } = props

return {
...props,
ContextComponent: withPqlFilterProvider(ContextComponent),
useSidebarOptions: withPqlFilterSidebar(useSidebarOptions),
useDataQueryHelper: withPqlFilterQuery(useDataQueryHelper)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { createContext, useContext, useMemo, useState } from 'react'

export interface IPqlFilterContext {
pqlFilter: string | null
setPqlFilter: (filter: string | null) => void
}

export const PqlFilterContext = createContext<IPqlFilterContext | undefined>(undefined)

export interface IPqlFilterProviderProps {
children: React.ReactNode
}

export const PqlFilterProvider = ({ children }: IPqlFilterProviderProps): React.JSX.Element => {
const [pqlFilter, setPqlFilter] = useState<string | null>(null)

return useMemo(() => (
<PqlFilterContext.Provider value={ { pqlFilter, setPqlFilter } }>
{children}
</PqlFilterContext.Provider>
), [pqlFilter, children])
}

export const usePqlFilterContext = (): IPqlFilterContext => {
const context = useContext(PqlFilterContext)

if (context === undefined) {
throw new Error('usePqlFilterContext must be used within a PqlFilterProvider')
}

return context
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react'
import { Content, ContentLayout, Header, Select } from '@pimcore/studio-ui-bundle/components'
import { usePqlFilterContext } from '../provider/pql-filter-provider'

export interface predefinedPqlFilter {
label: string
pql: string
}

export const PqlSidebar = (): React.JSX.Element => {
const { pqlFilter, setPqlFilter } = usePqlFilterContext()
const predefinedPqls: predefinedPqlFilter[] = [
{ label: 'All E-Type models which are green or produced before 1965.', pql: 'series = "E-Type" AND (color = "green" OR productionYear < 1965)' },
{ label: 'All Alfa cars produced after 1965', pql: 'manufacturer:Manufacturer.name = "Alfa" AND productionYear > 1965' },
{ label: 'All red or blue cars using standard PQL syntax.', pql: 'color = "red" OR color = "blue"' }
]

const options = predefinedPqls.map((filter) => ({
label: filter.label,
value: filter.pql
}))

return (
<ContentLayout>
<Content padded>
<Header title="Predefined PQL Filter" />

<Select
allowClear
className="w-full"
onChange={ setPqlFilter }
options={ options }
value={ pqlFilter }
/>
</Content>
</ContentLayout>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react'
import { type AbstractDecoratorProps } from '@pimcore/studio-ui-bundle/modules/element'
import { PqlFilterProvider } from './provider/pql-filter-provider'

export const withPqlFilterProvider = (ContextComponent: AbstractDecoratorProps['ContextComponent']): AbstractDecoratorProps['ContextComponent'] => {
const PqlContextComponent = (): React.JSX.Element => {
return (
<PqlFilterProvider>
<ContextComponent />
</PqlFilterProvider>
)
}

return PqlContextComponent
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { type AbstractDecoratorProps } from '@pimcore/studio-ui-bundle/modules/element'
import { usePqlFilterContext } from './provider/pql-filter-provider'

export const withPqlFilterQuery = (useBaseHook: AbstractDecoratorProps['useDataQueryHelper']): AbstractDecoratorProps['useDataQueryHelper'] => {
const usePqlDataQueryHelper: AbstractDecoratorProps['useDataQueryHelper'] = () => {
const { getArgs: baseGetArgs, ...baseProps } = useBaseHook()
const { pqlFilter } = usePqlFilterContext()

const getArgs: typeof baseGetArgs = () => {
const baseArgs = baseGetArgs()

return {
...baseArgs,
body: {
...baseArgs.body,
filters: {
...baseArgs.body?.filters,
columnFilters: [
...(baseArgs.body?.filters?.columnFilters ?? []),
...pqlFilter !== null
? [{
type: 'system.pql',
locale: null,
filterValue: pqlFilter
}]
: []
]
}
}
}
}

return {
...baseProps,
getArgs
}
}

return usePqlDataQueryHelper
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react'
import { type AbstractDecoratorProps } from '@pimcore/studio-ui-bundle/modules/element'
import { Icon } from '@pimcore/studio-ui-bundle/components'
import { PqlSidebar } from './sidebar/pql-sidebar'

export const withPqlFilterSidebar = (useBaseHook: AbstractDecoratorProps['useSidebarOptions']): AbstractDecoratorProps['useSidebarOptions'] => {
const usePqlFilterSidebarOptions: AbstractDecoratorProps['useSidebarOptions'] = () => {
const { getProps: baseGetProps } = useBaseHook()

const getProps: ReturnType<AbstractDecoratorProps['useSidebarOptions']>['getProps'] = () => {
const baseProps = baseGetProps()

baseProps.entries.unshift({
key: 'pql-filter',
component: <PqlSidebar />,
icon: <Icon value="pimcore" />
})

return baseProps
}

return {
getProps
}
}

return usePqlFilterSidebarOptions
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react'
import { Button } from '@pimcore/studio-ui-bundle/components'

export const ExtraButton = (): React.JSX.Element => {
return <Button>Injected Plugin Button</Button>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react'
import { Toolbar as BaseToolbar, Flex } from '@pimcore/studio-ui-bundle/components'
import { SlotRenderer } from '@pimcore/studio-ui-bundle/modules/app'

export const Toolbar = (): React.JSX.Element => {
return (
<BaseToolbar theme="secondary">
<Flex gap="small">
<SlotRenderer slot="carsListing.toolbar.left" />
</Flex>

<Flex gap="small">
<SlotRenderer slot="carsListing.toolbar.right" />
</Flex>
</BaseToolbar>
)
}
10 changes: 10 additions & 0 deletions assets/js/src/examples/listings/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { type IAbstractPlugin } from '@pimcore/studio-ui-bundle'
import { CustomDataObjectListingModule } from './modules/custom-data-object-listing'

export const ListingPlugin: IAbstractPlugin = {
name: 'ListingPlugin',

onStartup ({ moduleSystem }) {
moduleSystem.registerModule(CustomDataObjectListingModule)
}
}
120 changes: 120 additions & 0 deletions assets/js/src/examples/listings/modules/custom-data-object-listing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { container, type AbstractModule } from '@pimcore/studio-ui-bundle'
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
import { componentConfig, type ComponentRegistry, ComponentType, type MainNavRegistry } from '@pimcore/studio-ui-bundle/modules/app'
import { type WidgetRegistry } from '@pimcore/studio-ui-bundle/modules/widget-manager'
import { CustomListing } from '../widgets/custom-listing'
import { type ListingBuilder } from '@pimcore/studio-ui-bundle/modules/element'
import { type ClassDefinitionSelectionDecoratorConfig } from '@pimcore/studio-ui-bundle/modules/data-object'
import { PqlFilterDecorator } from '../components/custom-listing/decorator/pql-filter/pql-filter-decorator'
import { HeaderDecorator } from '../components/custom-listing/decorator/header/header-decorator'
import { Toolbar } from '../components/custom-listing/toolbar/toolbar'
import { ExtraButton } from '../components/custom-listing/toolbar/extra-button/extra-button'

export const CustomDataObjectListingModule: AbstractModule = {
onInit: (): void => {
const mainNavRegistryService = container.get<MainNavRegistry>(serviceIds.mainNavRegistry)

mainNavRegistryService.registerMainNavItem({
path: 'Example Plugin',
icon: 'pimcore'
})

mainNavRegistryService.registerMainNavItem({
path: 'Example Plugin/Listing Examples/Cars Listing',
widgetConfig: {
name: 'Cars',
id: 'cars-listing',
component: 'cars-listing',
config: {
icon: {
type: 'name',
value: 'car'
}
}
}
})

const widgetRegistryService = container.get<WidgetRegistry>(serviceIds.widgetManager)
widgetRegistryService.registerWidget({
name: 'cars-listing',
component: CustomListing
})

container.bind('Cars/Listing/Builder').toConstantValue(container.get<ListingBuilder>('DataObject/Listing/Builder').copy())
const newListingBuilder = container.get<ListingBuilder>('Cars/Listing/Builder')

newListingBuilder.addDecorator({
name: 'pql-filter',
decorator: PqlFilterDecorator
})

newListingBuilder.addDecorator({
name: 'header',
decorator: HeaderDecorator
})

// example usage of removing a decorator
newListingBuilder.removeDecorator('generalFilters')

// example usage of overriding a decorator
const classDefinitionSelectionDecorator = newListingBuilder.getDecorator('classDefinitionSelection')

if (classDefinitionSelectionDecorator !== undefined) {
const classDefinitionSelectionDecoratorConfig: ClassDefinitionSelectionDecoratorConfig = {
...classDefinitionSelectionDecorator.config,
...{
classRestriction: [{ classes: 'Car' }]
}
}

newListingBuilder.overrideDecorator({
name: 'classDefinitionSelection',
config: classDefinitionSelectionDecoratorConfig
})
}

const componentRegistry = container.get<ComponentRegistry>(serviceIds['App/ComponentRegistry/ComponentRegistry'])

componentRegistry.registerConfig({
carsListing: {
toolbar: {
component: {
name: 'carsListing.toolbar',
type: ComponentType.SINGLE
},
left: {
type: ComponentType.SLOT,
name: 'carsListing.toolbar.left'
},
right: {
type: ComponentType.SLOT,
name: 'carsListing.toolbar.right'
}
}
}
})

componentRegistry.register({
name: 'carsListing.toolbar',
component: Toolbar
})

const leftSlots = componentRegistry.getSlotComponents(componentConfig.dataObject.listing.toolbar.left.name)
const rightSlots = componentRegistry.getSlotComponents(componentConfig.dataObject.listing.toolbar.right.name)

for (const leftSlot of leftSlots) {
componentRegistry.registerToSlot('carsListing.toolbar.left', leftSlot)
}

componentRegistry.registerToSlot('carsListing.toolbar.right', {
name: 'extraButton',
component: ExtraButton
})

for (const rightSlot of rightSlots) {
componentRegistry.registerToSlot('carsListing.toolbar.right', rightSlot)
}

componentRegistry.getSlotComponents('carsListing.toolbar.left')
}
}
Loading