Skip to content

Commit

Permalink
Add URL slug data type (#820)
Browse files Browse the repository at this point in the history
  • Loading branch information
markus-moser authored Dec 11, 2024
1 parent 193dbcf commit b43b2a2
Show file tree
Hide file tree
Showing 33 changed files with 2,785 additions and 0 deletions.
3 changes: 3 additions & 0 deletions assets/build/api/openapi-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ const config: ConfigFile = {
},
'../../js/src/core/modules/data-object/unit-slice.gen.ts': {
filterEndpoints: pathMatcher(/\/unit\//i)
},
'../../js/src/core/modules/document/sites-slice.gen.ts': {
filterEndpoints: pathMatcher(/\/documents\/sites\//i)
}
},
exportName: 'api',
Expand Down
2 changes: 2 additions & 0 deletions assets/js/src/core/app/config/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import { DynamicTypeObjectDataEmail } from '@Pimcore/modules/element/dynamic-typ
import { DynamicTypeObjectDataGender } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-gender'
import { DynamicTypeObjectDataRgbaColor } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-rgba-color'
import { DynamicTypeObjectDataCheckbox } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-checkbox'
import { DynamicTypeObjectDataUrlSlug } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-url-slug'
import { DynamicTypeObjectDataDate } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-date'
import { DynamicTypeObjectDataDatetime } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-datetime'
import { DynamicTypeObjectDataDateRange } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-date-range'
Expand Down Expand Up @@ -245,6 +246,7 @@ container.bind(serviceIds['DynamicTypes/ObjectData/Email']).to(DynamicTypeObject
container.bind(serviceIds['DynamicTypes/ObjectData/Gender']).to(DynamicTypeObjectDataGender).inSingletonScope()
container.bind(serviceIds['DynamicTypes/ObjectData/RgbaColor']).to(DynamicTypeObjectDataRgbaColor).inSingletonScope()
container.bind(serviceIds['DynamicTypes/ObjectData/Checkbox']).to(DynamicTypeObjectDataCheckbox).inSingletonScope()
container.bind(serviceIds['DynamicTypes/ObjectData/UrlSlug']).to(DynamicTypeObjectDataUrlSlug).inSingletonScope()
container.bind(serviceIds['DynamicTypes/ObjectData/Date']).to(DynamicTypeObjectDataDate).inSingletonScope()
container.bind(serviceIds['DynamicTypes/ObjectData/Datetime']).to(DynamicTypeObjectDataDatetime).inSingletonScope()
container.bind(serviceIds['DynamicTypes/ObjectData/DateRange']).to(DynamicTypeObjectDataDateRange).inSingletonScope()
Expand Down
1 change: 1 addition & 0 deletions assets/js/src/core/app/config/services/service-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export const serviceIds = {
'DynamicTypes/ObjectData/Gender': 'DynamicTypes/ObjectData/Gender',
'DynamicTypes/ObjectData/RgbaColor': 'DynamicTypes/ObjectData/RgbaColor',
'DynamicTypes/ObjectData/Checkbox': 'DynamicTypes/ObjectData/Checkbox',
'DynamicTypes/ObjectData/UrlSlug': 'DynamicTypes/ObjectData/UrlSlug',
'DynamicTypes/ObjectData/Date': 'DynamicTypes/ObjectData/Date',
'DynamicTypes/ObjectData/Datetime': 'DynamicTypes/ObjectData/Datetime',
'DynamicTypes/ObjectData/DateRange': 'DynamicTypes/ObjectData/DateRange',
Expand Down
51 changes: 51 additions & 0 deletions assets/js/src/core/modules/document/hooks/use-sites.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Pimcore
*
* This source file is available under two different licenses:
* - Pimcore Open Core License (POCL)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license https://github.com/pimcore/studio-ui-bundle/blob/1.x/LICENSE.md POCL and PCL
*/

import { type Site, useDocumentsListAvailableSitesQuery } from '@Pimcore/modules/document/sites-slice.gen'

export interface UseSitesReturn {
getSiteById: (siteId: number) => Site | undefined
getAllSites: () => Site[]
getSitesByIds: (ids: number[]) => Site[]
getRemainingSites: (ids: number[], filteredSiteIds?: number[]) => Site[]
}

export const useSites = (): UseSitesReturn => {
const { data: sites } = useDocumentsListAvailableSitesQuery({ excludeMainSite: false })

const getSiteById = (siteId: number): Site | undefined => {
return sites?.items?.find(site => site.id === siteId)
}

const getAllSites = (): Site[] => {
return sites?.items ?? []
}

const getSitesByIds = (ids: number[]): Site[] => {
return sites?.items?.filter(site => ids.includes(site.id)) ?? []
}

const getRemainingSites = (ids: number[], filteredSiteIds?: number[]): Site[] => {
const filteredSites = filteredSiteIds !== undefined && filteredSiteIds.length > 0
? getSitesByIds(filteredSiteIds)
: sites?.items
return filteredSites?.filter(site => !ids.includes(site.id)) ?? []
}

return {
getSiteById,
getAllSites,
getSitesByIds,
getRemainingSites
}
}
56 changes: 56 additions & 0 deletions assets/js/src/core/modules/document/sites-slice.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { api } from "../../app/api/pimcore/index";
export const addTagTypes = ["Documents"] as const;
const injectedRtkApi = api
.enhanceEndpoints({
addTagTypes,
})
.injectEndpoints({
endpoints: (build) => ({
documentsListAvailableSites: build.query<
DocumentsListAvailableSitesApiResponse,
DocumentsListAvailableSitesApiArg
>({
query: (queryArg) => ({
url: `/pimcore-studio/api/documents/sites/list-available`,
params: { excludeMainSite: queryArg.excludeMainSite },
}),
providesTags: ["Documents"],
}),
}),
overrideExisting: false,
});
export { injectedRtkApi as api };
export type DocumentsListAvailableSitesApiResponse = /** status 200 List of available sites */ {
items: Site[];
};
export type DocumentsListAvailableSitesApiArg = {
/** Exclude main site from the list */
excludeMainSite?: boolean;
};
export type Site = {
/** AdditionalAttributes */
additionalAttributes?: {
[key: string]: string | number | boolean | object | any[];
};
/** ID */
id: number;
/** Domains */
domains: string[];
/** Domain */
domain: string;
/** ID of the root */
rootId?: number | null;
/** Root path */
rootPath?: string | null;
};
export type Error = {
/** Message */
message: string;
};
export type DevError = {
/** Message */
message: string;
/** Details */
details: string;
};
export const { useDocumentsListAvailableSitesQuery } = injectedRtkApi;
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Pimcore
*
* This source file is available under two different licenses:
* - Pimcore Open Core License (POCL)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license https://github.com/pimcore/studio-ui-bundle/blob/1.x/LICENSE.md POCL and PCL
*/

import React, { useEffect, useState } from 'react'
import { Input, List, Tooltip, Typography } from 'antd'
import { Flex } from '@Pimcore/components/flex/flex'
import { toCssDimension } from '@Pimcore/utils/css'
import { useSites } from '@Pimcore/modules/document/hooks/use-sites'
import { useTranslation } from 'react-i18next'
import { Dropdown } from '@Pimcore/components/dropdown/dropdown'
import { DropdownButton } from '@Pimcore/components/dropdown-button/dropdown-button'
import { IconButton } from '@Pimcore/components/icon-button/icon-button'

export interface UrlSlugEntry {
slug: string
siteId: number
}

export interface UrlSlugProps {
availableSites?: number[] | null
disabled?: boolean
domainLabelWidth?: number | null
width?: number | string | null
value?: UrlSlugEntry[] | null
onChange?: (value?: UrlSlugEntry[] | null) => void
}

export const UrlSlug = (props: UrlSlugProps): React.JSX.Element => {
const [value, setValue] = useState<UrlSlugEntry[]>(props.value ?? [])
const [errors, setErrors] = useState<boolean[]>([])
const { t } = useTranslation()
const { getSiteById, getRemainingSites } = useSites()
const { Text } = Typography

useEffect(() => {
if (props.onChange !== undefined) {
props.onChange(value)
}
}, [value])

const validateSlug = (slug: string): boolean => {
if (slug !== '') {
if (!slug.startsWith('/') || slug.length < 2) {
return false
}
slug = slug.substring(1).replace(/\/$/, '')
const parts = slug.split('/')
for (const part of parts) {
if (part.length === 0) {
return false
}
}
}
return true
}

const handleInputChange = (index: number, newSlug: string): void => {
const newValue = [...value]

newValue[index].slug = newSlug

const newErrors = [...errors]
newErrors[index] = !validateSlug(newSlug)
setValue(newValue)
setErrors(newErrors)
}

const remainingSites = getRemainingSites(value.map(item => item.siteId), props.availableSites ?? undefined)

const remainingSitesMenuItems = remainingSites.map(site => ({
key: site.id,
label: site.domain,
onClick: () => { setValue([...value, { slug: '', siteId: site.id }]) }
}))

return (
<List
bordered
dataSource={ value }
loadMore={ remainingSites.length > 0 && (
<List.Item>
<Dropdown
disabled={ props.disabled }
menu={ { items: remainingSitesMenuItems } }
trigger={ ['click'] }
>
<DropdownButton type="default">
{t('url-slug.add-site')}
</DropdownButton>
</Dropdown>
</List.Item>
) }
renderItem={ (item: UrlSlugEntry, index: number) => (
<List.Item>
<Flex
align="center"
className="w-full"
gap="small"
justify="center"
>
<div style={ { width: toCssDimension(props.domainLabelWidth, 250) } }>
{ item.siteId === 0 ? t('fallback') : getSiteById(item.siteId)?.domain }
</div>
<div className="w-full">
<Input
onChange={ e => { handleInputChange(index, e.target.value) } }
status={ errors[index] ? 'error' : undefined }
value={ item.slug }
/>
{ errors[index] && (
<Text type="danger">
{t('url-slug.invalid')}
</Text>
)}
</div>
<Tooltip title={ t('remove') }>
<IconButton
icon={ { value: 'trash' } }
onClick={ () => {
const newValue = [...value]
newValue.splice(index, 1)
setValue(newValue)
} }
/>
</Tooltip>
</Flex>
</List.Item>
) }
size="small"
style={ { maxWidth: toCssDimension(props.width) } }
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Pimcore
*
* This source file is available under two different licenses:
* - Pimcore Open Core License (POCL)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license https://github.com/pimcore/studio-ui-bundle/blob/1.x/LICENSE.md POCL and PCL
*/

import React from 'react'
import {
type AbstractObjectDataDefinition, DynamicTypeObjectDataAbstract
} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/dynamic-type-object-data-abstract'
import {
UrlSlug, type UrlSlugProps
} from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/components/url-slug/url-slug'

export type UrlSlugObjectDataDefinition = AbstractObjectDataDefinition & UrlSlugProps

export class DynamicTypeObjectDataUrlSlug extends DynamicTypeObjectDataAbstract {
id: string = 'urlSlug'

getObjectDataComponent (props: UrlSlugObjectDataDefinition): React.ReactElement<AbstractObjectDataDefinition> {
return (
<UrlSlug
{ ...props }
disabled={ props.noteditable === true }
/>
)
}
}
2 changes: 2 additions & 0 deletions assets/js/src/core/modules/element/dynamic-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ import { type DynamicTypeObjectDataEmail } from '@Pimcore/modules/element/dynami
import { type DynamicTypeObjectDataGender } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-gender'
import { type DynamicTypeObjectDataRgbaColor } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-rgba-color'
import { type DynamicTypeObjectDataCheckbox } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-checkbox'
import { type DynamicTypeObjectDataUrlSlug } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-url-slug'
import { type DynamicTypeObjectDataDate } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-date'
import { type DynamicTypeObjectDataDatetime } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-datetime'
import { type DynamicTypeObjectDataDateRange } from '@Pimcore/modules/element/dynamic-types/defintinitions/objects/data-related/types/dynamic-type-object-data-date-range'
Expand Down Expand Up @@ -213,6 +214,7 @@ moduleSystem.registerModule({
objectDataRegistry.registerDynamicType(container.get<DynamicTypeObjectDataGender>(serviceIds['DynamicTypes/ObjectData/Gender']))
objectDataRegistry.registerDynamicType(container.get<DynamicTypeObjectDataRgbaColor>(serviceIds['DynamicTypes/ObjectData/RgbaColor']))
objectDataRegistry.registerDynamicType(container.get<DynamicTypeObjectDataCheckbox>(serviceIds['DynamicTypes/ObjectData/Checkbox']))
objectDataRegistry.registerDynamicType(container.get<DynamicTypeObjectDataUrlSlug>(serviceIds['DynamicTypes/ObjectData/UrlSlug']))
objectDataRegistry.registerDynamicType(container.get<DynamicTypeObjectDataDate>(serviceIds['DynamicTypes/ObjectData/Date']))
objectDataRegistry.registerDynamicType(container.get<DynamicTypeObjectDataDatetime>(serviceIds['DynamicTypes/ObjectData/Datetime']))
objectDataRegistry.registerDynamicType(container.get<DynamicTypeObjectDataDateRange>(serviceIds['DynamicTypes/ObjectData/DateRange']))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"entrypoints": {
"vendor": {
"js": [
"/bundles/pimcorestudioui/build/006c3bf9-d70b-4e70-9ac5-1fb2a8c86d68/vendor.js"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"bundles/pimcorestudioui/build/006c3bf9-d70b-4e70-9ac5-1fb2a8c86d68/vendor.js": "/bundles/pimcorestudioui/build/006c3bf9-d70b-4e70-9ac5-1fb2a8c86d68/vendor.js"
}
2 changes: 2 additions & 0 deletions public/build/006c3bf9-d70b-4e70-9ac5-1fb2a8c86d68/vendor.js

Large diffs are not rendered by default.

Loading

0 comments on commit b43b2a2

Please sign in to comment.