Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add URL slug data type #820

Merged
merged 6 commits into from
Dec 11, 2024
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
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"
}

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions public/build/2c0a0853-46df-432c-a65b-f0b1ee58e475/core-dll.js

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions public/build/2c0a0853-46df-432c-a65b-f0b1ee58e475/entrypoints.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"entrypoints": {
"core-dll": {
"css": [
"/bundles/pimcorestudioui/build/2c0a0853-46df-432c-a65b-f0b1ee58e475/core-dll.css"
],
"js": [
"/bundles/pimcorestudioui/build/2c0a0853-46df-432c-a65b-f0b1ee58e475/core-dll.js"
]
}
}
}
Loading
Loading