@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.
Files changed (137) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/blocks/event/event-calendar-group/constants.d.ts +2 -0
  3. package/build/blocks/event/event-calendar-group/constants.d.ts.map +1 -0
  4. package/build/blocks/event/event-calendar-group/constants.js +2 -0
  5. package/build/blocks/event/event-calendar-group/edit.d.ts +6 -0
  6. package/build/blocks/event/event-calendar-group/edit.d.ts.map +1 -0
  7. package/build/blocks/event/event-calendar-group/edit.js +18 -0
  8. package/build/blocks/event/event-calendar-group/factory.d.ts +5 -0
  9. package/build/blocks/event/event-calendar-group/factory.d.ts.map +1 -0
  10. package/build/blocks/event/event-calendar-group/factory.js +25 -0
  11. package/build/blocks/event/event-calendar-group/icon.d.ts +2 -0
  12. package/build/blocks/event/event-calendar-group/icon.d.ts.map +1 -0
  13. package/build/blocks/event/event-calendar-group/icon.js +2 -0
  14. package/build/blocks/event/event-calendar-group/index.d.ts +6 -0
  15. package/build/blocks/event/event-calendar-group/index.d.ts.map +1 -0
  16. package/build/blocks/event/event-calendar-group/index.js +6 -0
  17. package/build/blocks/event/event-calendar-group/types.d.ts +12 -0
  18. package/build/blocks/event/event-calendar-group/types.d.ts.map +1 -0
  19. package/build/blocks/event/event-calendar-group/types.js +1 -0
  20. package/build/blocks/event/event-detail-content/constants.d.ts +2 -0
  21. package/build/blocks/event/event-detail-content/constants.d.ts.map +1 -0
  22. package/build/blocks/event/event-detail-content/constants.js +1 -0
  23. package/build/blocks/event/event-detail-content/edit.d.ts +6 -0
  24. package/build/blocks/event/event-detail-content/edit.d.ts.map +1 -0
  25. package/build/blocks/event/event-detail-content/edit.js +9 -0
  26. package/build/blocks/event/event-detail-content/factory.d.ts +5 -0
  27. package/build/blocks/event/event-detail-content/factory.d.ts.map +1 -0
  28. package/build/blocks/event/event-detail-content/factory.js +18 -0
  29. package/build/blocks/event/event-detail-content/icon.d.ts +2 -0
  30. package/build/blocks/event/event-detail-content/icon.d.ts.map +1 -0
  31. package/build/blocks/event/event-detail-content/icon.js +2 -0
  32. package/build/blocks/event/event-detail-content/index.d.ts +5 -0
  33. package/build/blocks/event/event-detail-content/index.d.ts.map +1 -0
  34. package/build/blocks/event/event-detail-content/index.js +5 -0
  35. package/build/blocks/event/event-detail-content/types.d.ts +6 -0
  36. package/build/blocks/event/event-detail-content/types.d.ts.map +1 -0
  37. package/build/blocks/event/event-detail-content/types.js +1 -0
  38. package/build/blocks/event/event-detail-header/constants.d.ts +2 -0
  39. package/build/blocks/event/event-detail-header/constants.d.ts.map +1 -0
  40. package/build/blocks/event/event-detail-header/constants.js +1 -0
  41. package/build/blocks/event/event-detail-header/edit.d.ts +6 -0
  42. package/build/blocks/event/event-detail-header/edit.d.ts.map +1 -0
  43. package/build/blocks/event/event-detail-header/edit.js +11 -0
  44. package/build/blocks/event/event-detail-header/factory.d.ts +5 -0
  45. package/build/blocks/event/event-detail-header/factory.d.ts.map +1 -0
  46. package/build/blocks/event/event-detail-header/factory.js +22 -0
  47. package/build/blocks/event/event-detail-header/icon.d.ts +2 -0
  48. package/build/blocks/event/event-detail-header/icon.d.ts.map +1 -0
  49. package/build/blocks/event/event-detail-header/icon.js +2 -0
  50. package/build/blocks/event/event-detail-header/index.d.ts +5 -0
  51. package/build/blocks/event/event-detail-header/index.d.ts.map +1 -0
  52. package/build/blocks/event/event-detail-header/index.js +5 -0
  53. package/build/blocks/event/event-detail-header/types.d.ts +9 -0
  54. package/build/blocks/event/event-detail-header/types.d.ts.map +1 -0
  55. package/build/blocks/event/event-detail-header/types.js +1 -0
  56. package/build/blocks/event/event-list-group/constants.d.ts +5 -0
  57. package/build/blocks/event/event-list-group/constants.d.ts.map +1 -0
  58. package/build/blocks/event/event-list-group/constants.js +5 -0
  59. package/build/blocks/event/event-list-group/edit.d.ts +6 -0
  60. package/build/blocks/event/event-list-group/edit.d.ts.map +1 -0
  61. package/build/blocks/event/event-list-group/edit.js +23 -0
  62. package/build/blocks/event/event-list-group/factory.d.ts +5 -0
  63. package/build/blocks/event/event-list-group/factory.d.ts.map +1 -0
  64. package/build/blocks/event/event-list-group/factory.js +28 -0
  65. package/build/blocks/event/event-list-group/icon.d.ts +2 -0
  66. package/build/blocks/event/event-list-group/icon.d.ts.map +1 -0
  67. package/build/blocks/event/event-list-group/icon.js +2 -0
  68. package/build/blocks/event/event-list-group/index.d.ts +8 -0
  69. package/build/blocks/event/event-list-group/index.d.ts.map +1 -0
  70. package/build/blocks/event/event-list-group/index.js +8 -0
  71. package/build/blocks/event/event-list-group/types.d.ts +30 -0
  72. package/build/blocks/event/event-list-group/types.d.ts.map +1 -0
  73. package/build/blocks/event/event-list-group/types.js +7 -0
  74. package/build/blocks/sections/base-section/attributes.d.ts +7 -0
  75. package/build/blocks/sections/base-section/attributes.d.ts.map +1 -1
  76. package/build/blocks/sections/base-section/attributes.js +7 -0
  77. package/build/blocks/sections/base-section/constants.d.ts +36 -0
  78. package/build/blocks/sections/base-section/constants.d.ts.map +1 -1
  79. package/build/blocks/sections/base-section/constants.js +16 -0
  80. package/build/blocks/sections/base-section/controls.d.ts.map +1 -1
  81. package/build/blocks/sections/base-section/controls.js +22 -4
  82. package/build/blocks/sections/base-section/edit.d.ts.map +1 -1
  83. package/build/blocks/sections/base-section/edit.js +21 -5
  84. package/build/blocks/sections/base-section/types.d.ts +14 -1
  85. package/build/blocks/sections/base-section/types.d.ts.map +1 -1
  86. package/build/components/Event/EntitySelectModal.d.ts +27 -0
  87. package/build/components/Event/EntitySelectModal.d.ts.map +1 -0
  88. package/build/components/Event/EntitySelectModal.js +100 -0
  89. package/build/components/Event/EventSelectModal.d.ts +7 -0
  90. package/build/components/Event/EventSelectModal.d.ts.map +1 -0
  91. package/build/components/Event/EventSelectModal.js +42 -0
  92. package/build/components/Event/EventTermSelectModal.d.ts +7 -0
  93. package/build/components/Event/EventTermSelectModal.d.ts.map +1 -0
  94. package/build/components/Event/EventTermSelectModal.js +48 -0
  95. package/build/index.d.ts +8 -0
  96. package/build/index.d.ts.map +1 -1
  97. package/build/index.js +4 -0
  98. package/build/types.d.ts +4 -0
  99. package/build/types.d.ts.map +1 -1
  100. package/package.json +1 -1
  101. package/src/blocks/event/event-calendar-group/constants.ts +3 -0
  102. package/src/blocks/event/event-calendar-group/edit.tsx +60 -0
  103. package/src/blocks/event/event-calendar-group/editor.scss +13 -0
  104. package/src/blocks/event/event-calendar-group/factory.tsx +30 -0
  105. package/src/blocks/event/event-calendar-group/icon.tsx +13 -0
  106. package/src/blocks/event/event-calendar-group/index.tsx +10 -0
  107. package/src/blocks/event/event-calendar-group/types.ts +13 -0
  108. package/src/blocks/event/event-detail-content/constants.ts +1 -0
  109. package/src/blocks/event/event-detail-content/edit.tsx +24 -0
  110. package/src/blocks/event/event-detail-content/factory.tsx +22 -0
  111. package/src/blocks/event/event-detail-content/icon.tsx +9 -0
  112. package/src/blocks/event/event-detail-content/index.ts +9 -0
  113. package/src/blocks/event/event-detail-content/types.ts +5 -0
  114. package/src/blocks/event/event-detail-header/constants.ts +2 -0
  115. package/src/blocks/event/event-detail-header/edit.tsx +49 -0
  116. package/src/blocks/event/event-detail-header/factory.tsx +26 -0
  117. package/src/blocks/event/event-detail-header/icon.tsx +8 -0
  118. package/src/blocks/event/event-detail-header/index.ts +9 -0
  119. package/src/blocks/event/event-detail-header/types.ts +9 -0
  120. package/src/blocks/event/event-list-group/constants.ts +6 -0
  121. package/src/blocks/event/event-list-group/edit.tsx +84 -0
  122. package/src/blocks/event/event-list-group/editor.scss +9 -0
  123. package/src/blocks/event/event-list-group/factory.tsx +33 -0
  124. package/src/blocks/event/event-list-group/icon.tsx +27 -0
  125. package/src/blocks/event/event-list-group/index.tsx +12 -0
  126. package/src/blocks/event/event-list-group/types.ts +27 -0
  127. package/src/blocks/sections/base-section/attributes.ts +7 -0
  128. package/src/blocks/sections/base-section/constants.ts +19 -0
  129. package/src/blocks/sections/base-section/controls.tsx +101 -1
  130. package/src/blocks/sections/base-section/edit.tsx +19 -3
  131. package/src/blocks/sections/base-section/types.ts +21 -1
  132. package/src/components/Event/EntitySelectModal.tsx +249 -0
  133. package/src/components/Event/EventSelectModal.tsx +51 -0
  134. package/src/components/Event/EventTermSelectModal.tsx +58 -0
  135. package/src/editor.scss +2 -0
  136. package/src/index.ts +36 -0
  137. 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
- ...(background ? {backgroundColor: background, padding: '16px'} : {}),
16
- ...(gradient ? {background: gradient, padding: '16px'} : {}),
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>