@brilo/base-blocks 0.1.35 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/.tsbuildinfo +1 -1
- package/build/blocks/event/event-calendar-group/constants.d.ts +2 -0
- package/build/blocks/event/event-calendar-group/constants.d.ts.map +1 -0
- package/build/blocks/event/event-calendar-group/constants.js +2 -0
- package/build/blocks/event/event-calendar-group/edit.d.ts +6 -0
- package/build/blocks/event/event-calendar-group/edit.d.ts.map +1 -0
- package/build/blocks/event/event-calendar-group/edit.js +18 -0
- package/build/blocks/event/event-calendar-group/factory.d.ts +5 -0
- package/build/blocks/event/event-calendar-group/factory.d.ts.map +1 -0
- package/build/blocks/event/event-calendar-group/factory.js +25 -0
- package/build/blocks/event/event-calendar-group/icon.d.ts +2 -0
- package/build/blocks/event/event-calendar-group/icon.d.ts.map +1 -0
- package/build/blocks/event/event-calendar-group/icon.js +2 -0
- package/build/blocks/event/event-calendar-group/index.d.ts +6 -0
- package/build/blocks/event/event-calendar-group/index.d.ts.map +1 -0
- package/build/blocks/event/event-calendar-group/index.js +6 -0
- package/build/blocks/event/event-calendar-group/types.d.ts +12 -0
- package/build/blocks/event/event-calendar-group/types.d.ts.map +1 -0
- package/build/blocks/event/event-calendar-group/types.js +1 -0
- package/build/blocks/event/event-detail-content/constants.d.ts +2 -0
- package/build/blocks/event/event-detail-content/constants.d.ts.map +1 -0
- package/build/blocks/event/event-detail-content/constants.js +1 -0
- package/build/blocks/event/event-detail-content/edit.d.ts +6 -0
- package/build/blocks/event/event-detail-content/edit.d.ts.map +1 -0
- package/build/blocks/event/event-detail-content/edit.js +9 -0
- package/build/blocks/event/event-detail-content/factory.d.ts +5 -0
- package/build/blocks/event/event-detail-content/factory.d.ts.map +1 -0
- package/build/blocks/event/event-detail-content/factory.js +18 -0
- package/build/blocks/event/event-detail-content/icon.d.ts +2 -0
- package/build/blocks/event/event-detail-content/icon.d.ts.map +1 -0
- package/build/blocks/event/event-detail-content/icon.js +2 -0
- package/build/blocks/event/event-detail-content/index.d.ts +5 -0
- package/build/blocks/event/event-detail-content/index.d.ts.map +1 -0
- package/build/blocks/event/event-detail-content/index.js +5 -0
- package/build/blocks/event/event-detail-content/types.d.ts +6 -0
- package/build/blocks/event/event-detail-content/types.d.ts.map +1 -0
- package/build/blocks/event/event-detail-content/types.js +1 -0
- package/build/blocks/event/event-detail-header/constants.d.ts +2 -0
- package/build/blocks/event/event-detail-header/constants.d.ts.map +1 -0
- package/build/blocks/event/event-detail-header/constants.js +1 -0
- package/build/blocks/event/event-detail-header/edit.d.ts +6 -0
- package/build/blocks/event/event-detail-header/edit.d.ts.map +1 -0
- package/build/blocks/event/event-detail-header/edit.js +11 -0
- package/build/blocks/event/event-detail-header/factory.d.ts +5 -0
- package/build/blocks/event/event-detail-header/factory.d.ts.map +1 -0
- package/build/blocks/event/event-detail-header/factory.js +22 -0
- package/build/blocks/event/event-detail-header/icon.d.ts +2 -0
- package/build/blocks/event/event-detail-header/icon.d.ts.map +1 -0
- package/build/blocks/event/event-detail-header/icon.js +2 -0
- package/build/blocks/event/event-detail-header/index.d.ts +5 -0
- package/build/blocks/event/event-detail-header/index.d.ts.map +1 -0
- package/build/blocks/event/event-detail-header/index.js +5 -0
- package/build/blocks/event/event-detail-header/types.d.ts +9 -0
- package/build/blocks/event/event-detail-header/types.d.ts.map +1 -0
- package/build/blocks/event/event-detail-header/types.js +1 -0
- package/build/blocks/event/event-list-group/constants.d.ts +5 -0
- package/build/blocks/event/event-list-group/constants.d.ts.map +1 -0
- package/build/blocks/event/event-list-group/constants.js +5 -0
- package/build/blocks/event/event-list-group/edit.d.ts +6 -0
- package/build/blocks/event/event-list-group/edit.d.ts.map +1 -0
- package/build/blocks/event/event-list-group/edit.js +23 -0
- package/build/blocks/event/event-list-group/factory.d.ts +5 -0
- package/build/blocks/event/event-list-group/factory.d.ts.map +1 -0
- package/build/blocks/event/event-list-group/factory.js +28 -0
- package/build/blocks/event/event-list-group/icon.d.ts +2 -0
- package/build/blocks/event/event-list-group/icon.d.ts.map +1 -0
- package/build/blocks/event/event-list-group/icon.js +2 -0
- package/build/blocks/event/event-list-group/index.d.ts +8 -0
- package/build/blocks/event/event-list-group/index.d.ts.map +1 -0
- package/build/blocks/event/event-list-group/index.js +8 -0
- package/build/blocks/event/event-list-group/types.d.ts +30 -0
- package/build/blocks/event/event-list-group/types.d.ts.map +1 -0
- package/build/blocks/event/event-list-group/types.js +7 -0
- package/build/blocks/sections/base-section/attributes.d.ts +7 -0
- package/build/blocks/sections/base-section/attributes.d.ts.map +1 -1
- package/build/blocks/sections/base-section/attributes.js +7 -0
- package/build/blocks/sections/base-section/constants.d.ts +36 -0
- package/build/blocks/sections/base-section/constants.d.ts.map +1 -1
- package/build/blocks/sections/base-section/constants.js +16 -0
- package/build/blocks/sections/base-section/controls.d.ts.map +1 -1
- package/build/blocks/sections/base-section/controls.js +22 -4
- package/build/blocks/sections/base-section/edit.d.ts.map +1 -1
- package/build/blocks/sections/base-section/edit.js +21 -5
- package/build/blocks/sections/base-section/types.d.ts +14 -1
- package/build/blocks/sections/base-section/types.d.ts.map +1 -1
- package/build/components/Event/EntitySelectModal.d.ts +27 -0
- package/build/components/Event/EntitySelectModal.d.ts.map +1 -0
- package/build/components/Event/EntitySelectModal.js +100 -0
- package/build/components/Event/EventSelectModal.d.ts +7 -0
- package/build/components/Event/EventSelectModal.d.ts.map +1 -0
- package/build/components/Event/EventSelectModal.js +42 -0
- package/build/components/Event/EventTermSelectModal.d.ts +7 -0
- package/build/components/Event/EventTermSelectModal.d.ts.map +1 -0
- package/build/components/Event/EventTermSelectModal.js +48 -0
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -0
- package/build/types.d.ts +4 -0
- package/build/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/blocks/event/event-calendar-group/constants.ts +3 -0
- package/src/blocks/event/event-calendar-group/edit.tsx +60 -0
- package/src/blocks/event/event-calendar-group/editor.scss +13 -0
- package/src/blocks/event/event-calendar-group/factory.tsx +30 -0
- package/src/blocks/event/event-calendar-group/icon.tsx +13 -0
- package/src/blocks/event/event-calendar-group/index.tsx +10 -0
- package/src/blocks/event/event-calendar-group/types.ts +13 -0
- package/src/blocks/event/event-detail-content/constants.ts +1 -0
- package/src/blocks/event/event-detail-content/edit.tsx +24 -0
- package/src/blocks/event/event-detail-content/factory.tsx +22 -0
- package/src/blocks/event/event-detail-content/icon.tsx +9 -0
- package/src/blocks/event/event-detail-content/index.ts +9 -0
- package/src/blocks/event/event-detail-content/types.ts +5 -0
- package/src/blocks/event/event-detail-header/constants.ts +2 -0
- package/src/blocks/event/event-detail-header/edit.tsx +49 -0
- package/src/blocks/event/event-detail-header/factory.tsx +26 -0
- package/src/blocks/event/event-detail-header/icon.tsx +8 -0
- package/src/blocks/event/event-detail-header/index.ts +9 -0
- package/src/blocks/event/event-detail-header/types.ts +9 -0
- package/src/blocks/event/event-list-group/constants.ts +6 -0
- package/src/blocks/event/event-list-group/edit.tsx +84 -0
- package/src/blocks/event/event-list-group/editor.scss +9 -0
- package/src/blocks/event/event-list-group/factory.tsx +33 -0
- package/src/blocks/event/event-list-group/icon.tsx +27 -0
- package/src/blocks/event/event-list-group/index.tsx +12 -0
- package/src/blocks/event/event-list-group/types.ts +27 -0
- package/src/blocks/sections/base-section/attributes.ts +7 -0
- package/src/blocks/sections/base-section/constants.ts +19 -0
- package/src/blocks/sections/base-section/controls.tsx +101 -1
- package/src/blocks/sections/base-section/edit.tsx +19 -3
- package/src/blocks/sections/base-section/types.ts +21 -1
- package/src/components/Event/EntitySelectModal.tsx +249 -0
- package/src/components/Event/EventSelectModal.tsx +51 -0
- package/src/components/Event/EventTermSelectModal.tsx +58 -0
- package/src/editor.scss +2 -0
- package/src/index.ts +36 -0
- package/src/types.ts +5 -0
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ButtonGroup,
|
|
6
6
|
PanelBody,
|
|
7
7
|
PanelRow,
|
|
8
|
+
RangeControl,
|
|
8
9
|
SelectControl,
|
|
9
10
|
TextControl,
|
|
10
11
|
__experimentalToggleGroupControl as ToggleGroupControl,
|
|
@@ -14,6 +15,9 @@ import {
|
|
|
14
15
|
import {__} from '@wordpress/i18n'
|
|
15
16
|
import {useTenantColorPalette} from '~/hooks/useTenantColorPalette'
|
|
16
17
|
import type {
|
|
18
|
+
BackgroundImage,
|
|
19
|
+
BackgroundImageFit,
|
|
20
|
+
BackgroundImagePosition,
|
|
17
21
|
BaseBlockAttributes,
|
|
18
22
|
Decoration,
|
|
19
23
|
DecorationDepth,
|
|
@@ -25,6 +29,9 @@ import type {
|
|
|
25
29
|
FluidDecorationImage,
|
|
26
30
|
} from './types'
|
|
27
31
|
import {
|
|
32
|
+
BACKGROUND_IMAGE_FITS,
|
|
33
|
+
BACKGROUND_IMAGE_OVERLAY_OPACITY_DEFAULT,
|
|
34
|
+
BACKGROUND_IMAGE_POSITIONS,
|
|
28
35
|
DECORATION_DEPTHS,
|
|
29
36
|
DECORATION_MAX_WIDTH_DEFAULT,
|
|
30
37
|
DECORATION_MAX_WIDTH_UNITS,
|
|
@@ -217,6 +224,93 @@ function DecorationControl({value, onChange}: DecorationControlProps) {
|
|
|
217
224
|
)
|
|
218
225
|
}
|
|
219
226
|
|
|
227
|
+
interface BackgroundImageControlProps {
|
|
228
|
+
value?: BackgroundImage
|
|
229
|
+
onChange: (value: BackgroundImage | undefined) => void
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function BackgroundImageControl({value, onChange}: BackgroundImageControlProps) {
|
|
233
|
+
const {paletteColors} = useTenantColorPalette()
|
|
234
|
+
const img = value?.img
|
|
235
|
+
const fit = value?.fit ?? 'cover'
|
|
236
|
+
const position = value?.position ?? 'center center'
|
|
237
|
+
const overlayColor = value?.overlay?.color
|
|
238
|
+
const overlayOpacity = value?.overlay?.opacity ?? BACKGROUND_IMAGE_OVERLAY_OPACITY_DEFAULT
|
|
239
|
+
|
|
240
|
+
const update = (patch: Partial<BackgroundImage>) => onChange({img, fit, position, overlay: value?.overlay, ...value, ...patch})
|
|
241
|
+
|
|
242
|
+
const handleSelect = (media: {id: number; url: string; alt: string; width?: number; height?: number}) => {
|
|
243
|
+
const nextImg: FluidDecorationImage = {id: media.id, src: media.url, alt: media.alt ?? '', width: media.width, height: media.height}
|
|
244
|
+
update({img: nextImg})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const handleRemove = () => onChange(undefined)
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<BaseControl label={__('Obrázek pozadí', 'brilo-blocks')} __nextHasNoMarginBottom>
|
|
251
|
+
<MediaUploadCheck>
|
|
252
|
+
<MediaUpload
|
|
253
|
+
onSelect={handleSelect}
|
|
254
|
+
allowedTypes={['image']}
|
|
255
|
+
value={img?.id || 0}
|
|
256
|
+
render={({open}: {open: () => void}) => (
|
|
257
|
+
<PanelRow>
|
|
258
|
+
{img?.src && <img src={img.src} alt={img.alt} style={{maxWidth: 48, maxHeight: 48, objectFit: 'contain'}} />}
|
|
259
|
+
<Button onClick={open} variant="secondary" size="small">
|
|
260
|
+
{img?.src ? __('Změnit', 'brilo-blocks') : __('Vybrat obrázek', 'brilo-blocks')}
|
|
261
|
+
</Button>
|
|
262
|
+
{!!img?.src && (
|
|
263
|
+
<Button onClick={handleRemove} variant="tertiary" size="small" isDestructive>
|
|
264
|
+
{__('Odebrat', 'brilo-blocks')}
|
|
265
|
+
</Button>
|
|
266
|
+
)}
|
|
267
|
+
</PanelRow>
|
|
268
|
+
)}
|
|
269
|
+
/>
|
|
270
|
+
</MediaUploadCheck>
|
|
271
|
+
|
|
272
|
+
{!!img?.src && (
|
|
273
|
+
<>
|
|
274
|
+
<ToggleGroupControl
|
|
275
|
+
label={__('Přizpůsobení', 'brilo-blocks')}
|
|
276
|
+
value={fit}
|
|
277
|
+
onChange={(next) => update({fit: next as BackgroundImageFit})}
|
|
278
|
+
isBlock
|
|
279
|
+
>
|
|
280
|
+
{BACKGROUND_IMAGE_FITS.map((f) => (
|
|
281
|
+
<ToggleGroupControlOption key={f.value} value={f.value} label={f.label} />
|
|
282
|
+
))}
|
|
283
|
+
</ToggleGroupControl>
|
|
284
|
+
|
|
285
|
+
<SelectControl<string>
|
|
286
|
+
label={__('Pozice', 'brilo-blocks')}
|
|
287
|
+
value={position}
|
|
288
|
+
options={[...BACKGROUND_IMAGE_POSITIONS]}
|
|
289
|
+
onChange={(next) => update({position: next as BackgroundImagePosition})}
|
|
290
|
+
/>
|
|
291
|
+
|
|
292
|
+
<BaseControl label={__('Překryv barvou', 'brilo-blocks')} __nextHasNoMarginBottom help={__('Ztmavení/zabarvení obrázku pro lepší čitelnost textu.', 'brilo-blocks')}>
|
|
293
|
+
<ColorPalette
|
|
294
|
+
colors={paletteColors}
|
|
295
|
+
value={overlayColor || undefined}
|
|
296
|
+
onChange={(next) => update({overlay: {color: next ?? undefined, opacity: overlayOpacity}})}
|
|
297
|
+
/>
|
|
298
|
+
{!!overlayColor && (
|
|
299
|
+
<RangeControl
|
|
300
|
+
label={__('Krytí překryvu', 'brilo-blocks')}
|
|
301
|
+
value={overlayOpacity}
|
|
302
|
+
min={0}
|
|
303
|
+
max={100}
|
|
304
|
+
onChange={(next) => update({overlay: {color: overlayColor, opacity: next ?? BACKGROUND_IMAGE_OVERLAY_OPACITY_DEFAULT}})}
|
|
305
|
+
/>
|
|
306
|
+
)}
|
|
307
|
+
</BaseControl>
|
|
308
|
+
</>
|
|
309
|
+
)}
|
|
310
|
+
</BaseControl>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
220
314
|
type SpacingPosition = BaseBlockAttributes['spacing']['position']
|
|
221
315
|
type SpacingSize = BaseBlockAttributes['spacing']['size']
|
|
222
316
|
|
|
@@ -227,7 +321,7 @@ interface BaseControlsProps {
|
|
|
227
321
|
}
|
|
228
322
|
|
|
229
323
|
export function BaseControls({attributes, setAttributes, exclude}: BaseControlsProps) {
|
|
230
|
-
const {id, spacing, background, textColor, decoration, decorations} = attributes
|
|
324
|
+
const {id, spacing, background, textColor, decoration, decorations, backgroundImage} = attributes
|
|
231
325
|
const {paletteColors, resolveHex} = useTenantColorPalette()
|
|
232
326
|
const fluid = decorations?.fluid ?? {}
|
|
233
327
|
|
|
@@ -277,6 +371,12 @@ export function BaseControls({attributes, setAttributes, exclude}: BaseControlsP
|
|
|
277
371
|
</PanelBody>
|
|
278
372
|
)}
|
|
279
373
|
|
|
374
|
+
{!exclude.includes('backgroundImage') && (
|
|
375
|
+
<PanelBody title={__('Obrázek pozadí', 'brilo-blocks')} initialOpen={false}>
|
|
376
|
+
<BackgroundImageControl value={backgroundImage} onChange={(value) => setAttributes({backgroundImage: value ?? {}})} />
|
|
377
|
+
</PanelBody>
|
|
378
|
+
)}
|
|
379
|
+
|
|
280
380
|
{!exclude.includes('decoration') && (
|
|
281
381
|
<PanelBody title={__('Dekorace', 'brilo-blocks')} initialOpen={false}>
|
|
282
382
|
<DecorationControl value={decoration} onChange={(value) => setAttributes({decoration: value ?? {}})} />
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {InspectorControls, useBlockProps, useInnerBlocksProps} from '@wordpress/block-editor'
|
|
2
2
|
import type {BlockEditProps} from '@wordpress/blocks'
|
|
3
|
+
import type {CSSProperties} from 'react'
|
|
3
4
|
import type {BaseBlockAttributes, BaseSectionOptions} from './types'
|
|
4
5
|
import {BaseControls} from './controls'
|
|
5
6
|
|
|
@@ -8,13 +9,28 @@ export function createBaseSectionEdit<TExtra extends Record<string, unknown> = R
|
|
|
8
9
|
|
|
9
10
|
return function Edit(props: BlockEditProps<BaseBlockAttributes & TExtra>) {
|
|
10
11
|
const {attributes, setAttributes} = props
|
|
11
|
-
const {background, textColor, gradient} = attributes as BaseBlockAttributes & {gradient?: string}
|
|
12
|
+
const {background, textColor, gradient, backgroundImage} = attributes as BaseBlockAttributes & {gradient?: string}
|
|
13
|
+
const bgImageSrc = backgroundImage?.img?.src
|
|
14
|
+
// A plain background-color/gradient always paints behind an image, so it can never tint it. To preview a
|
|
15
|
+
// translucent color/gradient sitting on top of the image (matching frontend), stack it as its own layer
|
|
16
|
+
// in front of the `url(...)` layer instead of setting backgroundColor separately.
|
|
17
|
+
const tintLayer = background ? `linear-gradient(${background}, ${background})` : gradient
|
|
12
18
|
const blockProps = useBlockProps({
|
|
13
19
|
className: options.blockClassName,
|
|
14
20
|
style: {
|
|
15
|
-
|
|
16
|
-
...(
|
|
21
|
+
position: 'relative' as CSSProperties['position'],
|
|
22
|
+
...(background && !bgImageSrc ? {backgroundColor: background, padding: '16px'} : {}),
|
|
23
|
+
...(gradient && !bgImageSrc ? {background: gradient, padding: '16px'} : {}),
|
|
17
24
|
...(textColor ? {color: textColor} : {}),
|
|
25
|
+
...(bgImageSrc
|
|
26
|
+
? {
|
|
27
|
+
backgroundImage: [tintLayer, `url(${bgImageSrc})`].filter(Boolean).join(', '),
|
|
28
|
+
backgroundSize: [tintLayer ? '100% 100%' : null, backgroundImage?.fit ?? 'cover'].filter(Boolean).join(', '),
|
|
29
|
+
backgroundPosition: [tintLayer ? 'center center' : null, backgroundImage?.position ?? 'center center'].filter(Boolean).join(', '),
|
|
30
|
+
backgroundRepeat: 'no-repeat',
|
|
31
|
+
padding: '16px',
|
|
32
|
+
}
|
|
33
|
+
: {}),
|
|
18
34
|
},
|
|
19
35
|
})
|
|
20
36
|
const innerBlocksProps = useInnerBlocksProps({className: options.containerClassName}, {
|
|
@@ -31,6 +31,25 @@ export interface Decoration {
|
|
|
31
31
|
offsetY?: string
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export type BackgroundImageFit = 'cover' | 'contain'
|
|
35
|
+
|
|
36
|
+
export type BackgroundImagePosition =
|
|
37
|
+
| 'left top' | 'center top' | 'right top'
|
|
38
|
+
| 'left center' | 'center center' | 'right center'
|
|
39
|
+
| 'left bottom' | 'center bottom' | 'right bottom'
|
|
40
|
+
|
|
41
|
+
export interface BackgroundImageOverlay {
|
|
42
|
+
color?: string
|
|
43
|
+
opacity?: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BackgroundImage {
|
|
47
|
+
img?: FluidDecorationImage
|
|
48
|
+
fit?: BackgroundImageFit
|
|
49
|
+
position?: BackgroundImagePosition
|
|
50
|
+
overlay?: BackgroundImageOverlay
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
export interface BaseBlockAttributes extends Record<string, unknown> {
|
|
35
54
|
id: string
|
|
36
55
|
spacing: {
|
|
@@ -46,9 +65,10 @@ export interface BaseBlockAttributes extends Record<string, unknown> {
|
|
|
46
65
|
bottom?: FluidDecoration
|
|
47
66
|
}
|
|
48
67
|
}
|
|
68
|
+
backgroundImage: BackgroundImage
|
|
49
69
|
}
|
|
50
70
|
|
|
51
|
-
export type ExcludableBaseAttr = 'spacing' | 'background' | 'id' | 'decorations' | 'decoration'
|
|
71
|
+
export type ExcludableBaseAttr = 'spacing' | 'background' | 'id' | 'decorations' | 'decoration' | 'backgroundImage'
|
|
52
72
|
|
|
53
73
|
export interface BaseSectionOptions<TExtra extends Record<string, unknown> = Record<string, never>> extends BriloBlockType {
|
|
54
74
|
supports?: BlockConfiguration['supports']
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import apiFetch from '@wordpress/api-fetch'
|
|
2
|
+
import {Button, Card, CardBody, __experimentalHStack as HStack, Modal, __experimentalVStack as VStack} from '@wordpress/components'
|
|
3
|
+
import {DataViews, type Field, type View} from '@wordpress/dataviews/wp'
|
|
4
|
+
import {__} from '@wordpress/i18n'
|
|
5
|
+
import {arrowDown, arrowUp, close, plusCircle} from '@wordpress/icons'
|
|
6
|
+
// biome-ignore lint/correctness/noUnusedImports: required in scope for classic JSX transform
|
|
7
|
+
import React, {useEffect, useMemo, useState} from 'react'
|
|
8
|
+
import type {SelectControlProps} from '~/types'
|
|
9
|
+
|
|
10
|
+
type EntityRecord = {id: number}
|
|
11
|
+
|
|
12
|
+
// `DataViews`' own prop type makes `getItemId` conditionally optional based on whether
|
|
13
|
+
// `Item extends {id: string}`, which TS can't resolve for an unresolved generic `T` — so
|
|
14
|
+
// re-type it here for a numeric-id `T`, where `getItemId` is always required.
|
|
15
|
+
type TypedDataViewsProps<T extends EntityRecord> = {
|
|
16
|
+
data: T[]
|
|
17
|
+
fields: Field<T>[]
|
|
18
|
+
view: View
|
|
19
|
+
onChangeView: (view: View) => void
|
|
20
|
+
paginationInfo: {totalItems: number; totalPages: number}
|
|
21
|
+
isLoading: boolean
|
|
22
|
+
defaultLayouts: {table: object}
|
|
23
|
+
getItemId: (item: T) => string
|
|
24
|
+
selection: string[]
|
|
25
|
+
onChangeSelection: (ids: string[]) => void
|
|
26
|
+
actions: Array<{
|
|
27
|
+
id: string
|
|
28
|
+
label: (items: T[]) => string
|
|
29
|
+
supportsBulk: boolean
|
|
30
|
+
callback: (items: T[]) => void
|
|
31
|
+
}>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TypedDataViews = DataViews as unknown as <T extends EntityRecord>(props: TypedDataViewsProps<T>) => JSX.Element
|
|
35
|
+
|
|
36
|
+
export type EntitySelectModalConfig<T extends EntityRecord> = {
|
|
37
|
+
endpoint: string
|
|
38
|
+
fields: Field<T>[]
|
|
39
|
+
defaultView: View
|
|
40
|
+
getItemLabel: (item: T) => string
|
|
41
|
+
matchesSearch: (item: T, needle: string) => boolean
|
|
42
|
+
sortValue: (item: T, field: string) => string | number
|
|
43
|
+
buttonLabel: string
|
|
44
|
+
modalTitle: string
|
|
45
|
+
emptyLabel: string
|
|
46
|
+
className?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function useEntities<T extends EntityRecord>(endpoint: string) {
|
|
50
|
+
const [entities, setEntities] = useState<T[]>([])
|
|
51
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
setIsLoading(true)
|
|
55
|
+
apiFetch<T[]>({path: endpoint})
|
|
56
|
+
.then((result) => setEntities(result))
|
|
57
|
+
.catch(() => setEntities([]))
|
|
58
|
+
.finally(() => setIsLoading(false))
|
|
59
|
+
}, [endpoint])
|
|
60
|
+
|
|
61
|
+
return {entities, isLoading}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function SelectedOrderList<T extends EntityRecord>({
|
|
65
|
+
items,
|
|
66
|
+
getItemLabel,
|
|
67
|
+
onChange,
|
|
68
|
+
onRemove,
|
|
69
|
+
emptyLabel,
|
|
70
|
+
}: {
|
|
71
|
+
items: T[]
|
|
72
|
+
getItemLabel: (item: T) => string
|
|
73
|
+
onChange: (ids: number[]) => void
|
|
74
|
+
onRemove: (id: number) => void
|
|
75
|
+
emptyLabel: string
|
|
76
|
+
}) {
|
|
77
|
+
if (0 === items.length) {
|
|
78
|
+
return <p className="i-event-select-modal__empty">{emptyLabel}</p>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const move = (index: number, direction: -1 | 1) => {
|
|
82
|
+
const target = index + direction
|
|
83
|
+
|
|
84
|
+
if (target < 0 || target >= items.length) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const next = [...items]
|
|
89
|
+
;[next[index], next[target]] = [next[target], next[index]]
|
|
90
|
+
onChange(next.map((item) => item.id))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<VStack spacing={2}>
|
|
95
|
+
{items.map((item, index) => (
|
|
96
|
+
<Card size="small" className="i-event-select-modal__item" key={item.id}>
|
|
97
|
+
<CardBody>
|
|
98
|
+
<HStack alignment="center">
|
|
99
|
+
<span
|
|
100
|
+
className="i-event-select-modal__item-title"
|
|
101
|
+
style={{
|
|
102
|
+
flexGrow: 1,
|
|
103
|
+
minWidth: 0,
|
|
104
|
+
textAlign: 'left',
|
|
105
|
+
overflow: 'hidden',
|
|
106
|
+
textOverflow: 'ellipsis',
|
|
107
|
+
whiteSpace: 'nowrap',
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{getItemLabel(item) || __('(bez názvu)', 'brilo-blocks')}
|
|
111
|
+
</span>
|
|
112
|
+
|
|
113
|
+
<HStack expanded={false} spacing={1}>
|
|
114
|
+
<Button
|
|
115
|
+
size="small"
|
|
116
|
+
icon={arrowUp}
|
|
117
|
+
label={__('Posunout výš', 'brilo-blocks')}
|
|
118
|
+
onClick={() => move(index, -1)}
|
|
119
|
+
disabled={0 === index}
|
|
120
|
+
/>
|
|
121
|
+
<Button
|
|
122
|
+
size="small"
|
|
123
|
+
icon={arrowDown}
|
|
124
|
+
label={__('Posunout níž', 'brilo-blocks')}
|
|
125
|
+
onClick={() => move(index, 1)}
|
|
126
|
+
disabled={index === items.length - 1}
|
|
127
|
+
/>
|
|
128
|
+
<Button size="small" icon={close} isDestructive label={__('Odebrat', 'brilo-blocks')} onClick={() => onRemove(item.id)} />
|
|
129
|
+
</HStack>
|
|
130
|
+
</HStack>
|
|
131
|
+
</CardBody>
|
|
132
|
+
</Card>
|
|
133
|
+
))}
|
|
134
|
+
</VStack>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Generic table-view entity picker built on @wordpress/dataviews (search, sort,
|
|
140
|
+
* checkbox selection, reorderable selection list). Feed it a REST endpoint +
|
|
141
|
+
* DataViews field config to get an `EventSelectModal`/`EventTermSelectModal`-style
|
|
142
|
+
* component without duplicating the DataViews wiring per entity type.
|
|
143
|
+
*/
|
|
144
|
+
export function createEntitySelectModal<T extends EntityRecord>(config: EntitySelectModalConfig<T>) {
|
|
145
|
+
return function EntitySelectModal({selectedItems, onChange}: SelectControlProps) {
|
|
146
|
+
const {entities, isLoading} = useEntities<T>(config.endpoint)
|
|
147
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
148
|
+
const [view, setView] = useState<View>(config.defaultView)
|
|
149
|
+
|
|
150
|
+
const selectedEntities = useMemo(
|
|
151
|
+
() => selectedItems.map((id) => entities.find((entity) => entity.id === id)).filter((entity): entity is T => undefined !== entity),
|
|
152
|
+
[selectedItems, entities],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const {data, paginationInfo} = useMemo(() => {
|
|
156
|
+
let filtered = entities
|
|
157
|
+
|
|
158
|
+
if (view.search) {
|
|
159
|
+
const needle = view.search.toLowerCase()
|
|
160
|
+
filtered = filtered.filter((entity) => config.matchesSearch(entity, needle))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (view.sort?.field) {
|
|
164
|
+
const {field, direction} = view.sort
|
|
165
|
+
const sorted = [...filtered]
|
|
166
|
+
|
|
167
|
+
sorted.sort((a, b) => {
|
|
168
|
+
const valueA = config.sortValue(a, field)
|
|
169
|
+
const valueB = config.sortValue(b, field)
|
|
170
|
+
const result =
|
|
171
|
+
typeof valueA === 'number' && typeof valueB === 'number' ? valueA - valueB : String(valueA).localeCompare(String(valueB))
|
|
172
|
+
|
|
173
|
+
return 'desc' === direction ? -result : result
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
filtered = sorted
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const perPage = view.perPage ?? 20
|
|
180
|
+
const page = view.page ?? 1
|
|
181
|
+
const start = (page - 1) * perPage
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
data: filtered.slice(start, start + perPage),
|
|
185
|
+
paginationInfo: {totalItems: filtered.length, totalPages: Math.ceil(filtered.length / perPage) || 1},
|
|
186
|
+
}
|
|
187
|
+
}, [entities, view])
|
|
188
|
+
|
|
189
|
+
const toggleSelected = (id: number) => {
|
|
190
|
+
onChange(selectedItems.includes(id) ? selectedItems.filter((itemId) => itemId !== id) : [...selectedItems, id])
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<VStack spacing={3} className={config.className ?? 'i-event-select-modal'}>
|
|
195
|
+
<Button variant="secondary" icon={plusCircle} onClick={() => setIsOpen(true)}>
|
|
196
|
+
{config.buttonLabel}
|
|
197
|
+
</Button>
|
|
198
|
+
|
|
199
|
+
<SelectedOrderList
|
|
200
|
+
items={selectedEntities}
|
|
201
|
+
getItemLabel={config.getItemLabel}
|
|
202
|
+
emptyLabel={config.emptyLabel}
|
|
203
|
+
onChange={onChange}
|
|
204
|
+
onRemove={(id) => onChange(selectedItems.filter((itemId) => itemId !== id))}
|
|
205
|
+
/>
|
|
206
|
+
|
|
207
|
+
{isOpen && (
|
|
208
|
+
<Modal title={config.modalTitle} onRequestClose={() => setIsOpen(false)} size="fill">
|
|
209
|
+
<VStack spacing={4} style={{height: '100%'}}>
|
|
210
|
+
<div style={{flexGrow: 1, minHeight: 0}}>
|
|
211
|
+
<TypedDataViews<T>
|
|
212
|
+
data={data}
|
|
213
|
+
fields={config.fields}
|
|
214
|
+
view={view}
|
|
215
|
+
onChangeView={setView}
|
|
216
|
+
paginationInfo={paginationInfo}
|
|
217
|
+
isLoading={isLoading}
|
|
218
|
+
defaultLayouts={{table: {}}}
|
|
219
|
+
getItemId={(item) => String(item.id)}
|
|
220
|
+
selection={selectedItems.map(String)}
|
|
221
|
+
onChangeSelection={(ids) => onChange(ids.map(Number))}
|
|
222
|
+
actions={[
|
|
223
|
+
{
|
|
224
|
+
id: 'toggle',
|
|
225
|
+
label: (items) =>
|
|
226
|
+
items.length && selectedItems.includes(items[0].id) ? __('Odebrat', 'brilo-blocks') : __('Přidat', 'brilo-blocks'),
|
|
227
|
+
supportsBulk: true,
|
|
228
|
+
callback: (items) => {
|
|
229
|
+
items.forEach((item) => {
|
|
230
|
+
toggleSelected(item.id)
|
|
231
|
+
})
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
]}
|
|
235
|
+
/>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<HStack justify="flex-end">
|
|
239
|
+
<Button variant="primary" onClick={() => setIsOpen(false)}>
|
|
240
|
+
{__('Hotovo', 'brilo-blocks')}
|
|
241
|
+
</Button>
|
|
242
|
+
</HStack>
|
|
243
|
+
</VStack>
|
|
244
|
+
</Modal>
|
|
245
|
+
)}
|
|
246
|
+
</VStack>
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type {View} from '@wordpress/dataviews/wp'
|
|
2
|
+
import {__} from '@wordpress/i18n'
|
|
3
|
+
import {createEntitySelectModal} from './EntitySelectModal'
|
|
4
|
+
|
|
5
|
+
type EventRecord = {
|
|
6
|
+
id: number
|
|
7
|
+
title: {rendered: string}
|
|
8
|
+
date: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DEFAULT_VIEW: View = {
|
|
12
|
+
type: 'table',
|
|
13
|
+
search: '',
|
|
14
|
+
filters: [],
|
|
15
|
+
page: 1,
|
|
16
|
+
perPage: 20,
|
|
17
|
+
sort: {field: 'title', direction: 'asc'},
|
|
18
|
+
fields: ['date'],
|
|
19
|
+
titleField: 'title',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Manual brilo_event picker (table view, search, sort, checkbox selection,
|
|
24
|
+
* reorderable selection list) with title/date columns. See `EntitySelectModal`
|
|
25
|
+
* for the shared DataViews wiring.
|
|
26
|
+
*/
|
|
27
|
+
export const EventSelectModal = createEntitySelectModal<EventRecord>({
|
|
28
|
+
endpoint: '/wp/v2/brilo_event?per_page=100&orderby=title&order=asc&_fields=id,title,date',
|
|
29
|
+
defaultView: DEFAULT_VIEW,
|
|
30
|
+
buttonLabel: __('Vybrat události', 'brilo-blocks'),
|
|
31
|
+
modalTitle: __('Vybrat události', 'brilo-blocks'),
|
|
32
|
+
emptyLabel: __('Nevybrány žádné události.', 'brilo-blocks'),
|
|
33
|
+
getItemLabel: (item) => item.title.rendered,
|
|
34
|
+
matchesSearch: (item, needle) => item.title.rendered.toLowerCase().includes(needle),
|
|
35
|
+
sortValue: (item, field) => ('title' === field ? item.title.rendered : ((item as never)[field] ?? '')),
|
|
36
|
+
fields: [
|
|
37
|
+
{
|
|
38
|
+
id: 'title',
|
|
39
|
+
label: __('Název', 'brilo-blocks'),
|
|
40
|
+
enableHiding: false,
|
|
41
|
+
enableSorting: true,
|
|
42
|
+
render: ({item}: {item: EventRecord}) => item.title.rendered || __('(bez názvu)', 'brilo-blocks'),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'date',
|
|
46
|
+
label: __('Datum', 'brilo-blocks'),
|
|
47
|
+
enableSorting: true,
|
|
48
|
+
render: ({item}: {item: EventRecord}) => (item.date ? new Date(item.date).toLocaleDateString() : '—'),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type {View} from '@wordpress/dataviews/wp'
|
|
2
|
+
import {__} from '@wordpress/i18n'
|
|
3
|
+
import {createEntitySelectModal} from './EntitySelectModal'
|
|
4
|
+
|
|
5
|
+
type EventCategoryRecord = {
|
|
6
|
+
id: number
|
|
7
|
+
name: string
|
|
8
|
+
slug: string
|
|
9
|
+
count?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_VIEW: View = {
|
|
13
|
+
type: 'table',
|
|
14
|
+
search: '',
|
|
15
|
+
filters: [],
|
|
16
|
+
page: 1,
|
|
17
|
+
perPage: 20,
|
|
18
|
+
sort: {field: 'name', direction: 'asc'},
|
|
19
|
+
fields: ['slug', 'count'],
|
|
20
|
+
titleField: 'name',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Manual brilo_event_category picker (table view, search, sort, checkbox
|
|
25
|
+
* selection, reorderable selection list) with name/slug/count columns. See
|
|
26
|
+
* `EntitySelectModal` for the shared DataViews wiring.
|
|
27
|
+
*/
|
|
28
|
+
export const EventTermSelectModal = createEntitySelectModal<EventCategoryRecord>({
|
|
29
|
+
endpoint: '/wp/v2/brilo_event_category?per_page=100&orderby=name&order=asc&_fields=id,name,slug,count',
|
|
30
|
+
defaultView: DEFAULT_VIEW,
|
|
31
|
+
buttonLabel: __('Vybrat kategorie', 'brilo-blocks'),
|
|
32
|
+
modalTitle: __('Vybrat kategorie', 'brilo-blocks'),
|
|
33
|
+
emptyLabel: __('Nevybrány žádné kategorie.', 'brilo-blocks'),
|
|
34
|
+
getItemLabel: (item) => item.name,
|
|
35
|
+
matchesSearch: (item, needle) => item.name.toLowerCase().includes(needle) || item.slug.toLowerCase().includes(needle),
|
|
36
|
+
sortValue: (item, field) => (item as Record<string, unknown>)[field] as string | number,
|
|
37
|
+
fields: [
|
|
38
|
+
{
|
|
39
|
+
id: 'name',
|
|
40
|
+
label: __('Název', 'brilo-blocks'),
|
|
41
|
+
enableHiding: false,
|
|
42
|
+
enableSorting: true,
|
|
43
|
+
render: ({item}: {item: EventCategoryRecord}) => item.name || __('(bez názvu)', 'brilo-blocks'),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'slug',
|
|
47
|
+
label: __('Slug', 'brilo-blocks'),
|
|
48
|
+
enableSorting: true,
|
|
49
|
+
render: ({item}: {item: EventCategoryRecord}) => item.slug || '—',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'count',
|
|
53
|
+
label: __('Počet', 'brilo-blocks'),
|
|
54
|
+
enableSorting: true,
|
|
55
|
+
render: ({item}: {item: EventCategoryRecord}) => (undefined !== item.count ? String(item.count) : '—'),
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
})
|
package/src/editor.scss
CHANGED
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
@forward "./blocks/usp/usp-item/editor";
|
|
37
37
|
@forward "./blocks/person/person-group/editor";
|
|
38
38
|
@forward "./blocks/person/person-item/editor";
|
|
39
|
+
@forward "./blocks/event/event-calendar-group/editor";
|
|
40
|
+
@forward "./blocks/event/event-list-group/editor";
|
|
39
41
|
@forward "./blocks/responsive-grid/responsive-grid-item/editor";
|
|
40
42
|
@forward "./blocks/layout/layout-aside/editor";
|
|
41
43
|
@forward "./blocks/layout/layout-content/editor";
|
package/src/index.ts
CHANGED
|
@@ -166,6 +166,42 @@ export type {ContentAttributes, ContentOptions} from './blocks/layout/layout-con
|
|
|
166
166
|
export {createContentBlock, contentDefaults} from './blocks/layout/layout-content'
|
|
167
167
|
export type {AsideAttributes, AsideOptions} from './blocks/layout/layout-aside'
|
|
168
168
|
export {createAsideBlock, asideDefaults} from './blocks/layout/layout-aside'
|
|
169
|
+
export type {
|
|
170
|
+
EventCalendarGroupAttributes,
|
|
171
|
+
EventCalendarGroupOptions,
|
|
172
|
+
} from './blocks/event/event-calendar-group'
|
|
173
|
+
export {
|
|
174
|
+
createEventCalendarGroupBlock,
|
|
175
|
+
eventCalendarGroupDefaults,
|
|
176
|
+
} from './blocks/event/event-calendar-group'
|
|
177
|
+
export type {
|
|
178
|
+
EventListGroupAttributes,
|
|
179
|
+
EventListGroupMode,
|
|
180
|
+
EventListGroupOptions,
|
|
181
|
+
} from './blocks/event/event-list-group'
|
|
182
|
+
export type {
|
|
183
|
+
EventDetailHeaderAttributes,
|
|
184
|
+
EventDetailHeaderOptions,
|
|
185
|
+
} from './blocks/event/event-detail-header'
|
|
186
|
+
export {
|
|
187
|
+
createEventDetailHeaderBlock,
|
|
188
|
+
eventDetailHeaderDefaults,
|
|
189
|
+
} from './blocks/event/event-detail-header'
|
|
190
|
+
export type {
|
|
191
|
+
EventDetailContentAttributes,
|
|
192
|
+
EventDetailContentOptions,
|
|
193
|
+
} from './blocks/event/event-detail-content'
|
|
194
|
+
export {
|
|
195
|
+
createEventDetailContentBlock,
|
|
196
|
+
eventDetailContentDefaults,
|
|
197
|
+
} from './blocks/event/event-detail-content'
|
|
198
|
+
export {
|
|
199
|
+
createEventListGroupBlock,
|
|
200
|
+
eventListGroupDefaults,
|
|
201
|
+
EVENT_LIST_GROUP_MODES,
|
|
202
|
+
EventSelectModal,
|
|
203
|
+
EventTermSelectModal,
|
|
204
|
+
} from './blocks/event/event-list-group'
|
|
169
205
|
export type {
|
|
170
206
|
JobPositionGroupAttributes,
|
|
171
207
|
JobPositionGroupOptions,
|
package/src/types.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface Icon {
|
|
|
17
17
|
media: Media
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export interface SelectControlProps {
|
|
21
|
+
selectedItems: number[]
|
|
22
|
+
onChange: (ids: number[]) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
export type OptionArray = ReadonlyArray<{value: string; label: string}>
|
|
21
26
|
export type NumberArray = ReadonlyArray<number>
|
|
22
27
|
export type StringArray = ReadonlyArray<string>
|