Skip to content

Commit

Permalink
Merge pull request #692 from contember/feat/blockrepeater
Browse files Browse the repository at this point in the history
block repeater
  • Loading branch information
matej21 authored Apr 22, 2024
2 parents bcf74f3 + 9dd9f6d commit 9918ebe
Show file tree
Hide file tree
Showing 50 changed files with 1,405 additions and 53 deletions.
2 changes: 1 addition & 1 deletion build/api/interface.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export interface DeleteEntityTriggerProps {
// (undocumented)
children: ReactNode;
// (undocumented)
immediatePersist?: true;
immediatePersist?: boolean;
// (undocumented)
onPersistError?: (result: ErrorPersistResult) => void;
// (undocumented)
Expand Down
76 changes: 76 additions & 0 deletions build/api/react-block-repeater.api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
## API Report File for "@contember/react-block-repeater"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts

import { Context } from 'react';
import { EntityAccessor } from '@contember/react-binding';
import { JSX as JSX_2 } from 'react/jsx-runtime';
import { NamedExoticComponent } from 'react';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
import { RepeaterAddItemIndex } from '@contember/react-repeater';
import { RepeaterProps } from '@contember/react-repeater';
import { SugaredRelativeSingleField } from '@contember/react-binding';

// @public (undocumented)
export const Block: NamedExoticComponent<BlockProps>;

// @public (undocumented)
export interface BlockProps {
// (undocumented)
children: ReactNode;
// (undocumented)
form?: ReactNode;
// (undocumented)
label?: ReactNode;
// (undocumented)
name: string;
}

// @public (undocumented)
export const BlockRepeater: NamedExoticComponent<BlockRepeaterProps>;

// @public (undocumented)
export const BlockRepeaterAddItemTrigger: ({ preprocess, index, type, ...props }: BlockRepeaterAddItemTriggerProps) => JSX_2.Element;

// @public (undocumented)
export interface BlockRepeaterAddItemTriggerProps {
// (undocumented)
children: ReactElement;
// (undocumented)
index?: RepeaterAddItemIndex;
// (undocumented)
preprocess?: EntityAccessor.BatchUpdatesHandler;
// (undocumented)
type: string;
}

// @internal (undocumented)
export const BlockRepeaterConfigContext: Context< {
discriminatedBy: SugaredRelativeSingleField['field'];
blocks: BlocksMap;
}>;

// @public (undocumented)
export type BlockRepeaterProps = {
sortableBy: RepeaterProps['sortableBy'];
discriminationField: SugaredRelativeSingleField['field'];
} & RepeaterProps;

// @public (undocumented)
export type BlocksMap = Record<string, BlockProps>;

// @public (undocumented)
export const useBlockRepeaterConfig: () => {
discriminatedBy: SugaredRelativeSingleField['field'];
blocks: BlocksMap;
};

// @public (undocumented)
export const useBlockRepeaterCurrentBlock: () => BlockProps | undefined;

// (No @packageDocumentation comment for this package)

```
10 changes: 7 additions & 3 deletions build/api/react-repeater.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ export type RepeaterAddItemIndex = number | 'first' | 'last' | undefined;
export type RepeaterAddItemMethod = (index: RepeaterAddItemIndex, preprocess?: EntityAccessor.BatchUpdatesHandler) => void;

// @public (undocumented)
export const RepeaterAddItemTrigger: ({ children, index }: {
export const RepeaterAddItemTrigger: ({ children, index, preprocess }: RepeaterAddItemTriggerProps) => JSX_2.Element;

// @public (undocumented)
export type RepeaterAddItemTriggerProps = {
children: ReactNode;
index: RepeaterAddItemIndex;
}) => JSX_2.Element;
index?: RepeaterAddItemIndex;
preprocess?: EntityAccessor.BatchUpdatesHandler;
};

// @internal (undocumented)
export const RepeaterCurrentEntityContext: Context<EntityAccessor_2>;
Expand Down
1 change: 1 addition & 0 deletions ee/admin-server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ COPY ./packages/playground/package.json ././packages/playground/package.json
COPY ./packages/react-auto/package.json ././packages/react-auto/package.json
COPY ./packages/react-binding/package.json ././packages/react-binding/package.json
COPY ./packages/react-binding-ui/package.json ././packages/react-binding-ui/package.json
COPY ./packages/react-block-repeater/package.json ././packages/react-block-repeater/package.json
COPY ./packages/react-board/package.json ././packages/react-board/package.json
COPY ./packages/react-board-dnd-kit/package.json ././packages/react-board-dnd-kit/package.json
COPY ./packages/react-choice-field/package.json ././packages/react-choice-field/package.json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ErrorPersistResult, SuccessfulPersistResult, useEntity, useMutationStat
const SlotButton = Slot as ComponentType<React.ButtonHTMLAttributes<HTMLButtonElement>>

export interface DeleteEntityTriggerProps {
immediatePersist?: true
immediatePersist?: boolean
children: ReactNode
onPersistSuccess?: (result: SuccessfulPersistResult) => void
onPersistError?: (result: ErrorPersistResult) => void
Expand Down
15 changes: 14 additions & 1 deletion packages/playground/admin/app/components/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ export const Navigation = () => {
<MenuItem icon={line} label={'Dynamic columns'} to={'board/assignee'} />
<MenuItem icon={line} label={'Static columns'} to={'board/status'} />
</MenuItem>
<MenuItem icon={<GripVertical size={16} />} label={'Repeater'} to={'repeater'} />
<MenuItem icon={<GripVertical size={16} />} label={'Repeater'}>
<MenuItem icon={line} label={'Sortable repeater'} to={'repeater'} />
<MenuItem icon={line} label={'Non-sortable repeater'} to={'repeater/nonSortable'} />
<MenuItem icon={line} label={'Block repeater'} to={'blocks'} />
<MenuItem icon={line} label={'Block repeater w/o dual render'} to={'blocks/withoutDualRender'} />
</MenuItem>
<MenuItem icon={<TableIcon size={16} />} label={'Grid'}>
<MenuItem icon={line} label={'Complex grid'} to={'grid'} />
<MenuItem icon={line} label={'Simple grid'} to={'grid/simpleGrid'} />
Expand All @@ -35,6 +40,14 @@ export const Navigation = () => {
<MenuItem icon={line} label={'Has many select'} to={'select/hasMany'} />
<MenuItem icon={line} label={'Has many sortable select'} to={'select/hasManySortable'} />
</MenuItem>
<MenuItem icon={<UploadIcon size={16} />} label={'Upload'}>
<MenuItem icon={line} label={'Image upload'} to={'upload/image'} />
<MenuItem icon={line} label={'Image w/o meta'} to={'upload/imageTrivial'} />
<MenuItem icon={line} label={'Audio upload'} to={'upload/audio'} />
<MenuItem icon={line} label={'Video upload'} to={'upload/video'} />
<MenuItem icon={line} label={'Generic file upload'} to={'upload/any'} />
<MenuItem icon={line} label={'Image repeater'} to={'upload/imageList'} />
</MenuItem>
<MenuItem icon={<LanguagesIcon size={16} />} label={'Dimensions'} to={'dimensions'} />
</Menu>
</div>
Expand Down
193 changes: 193 additions & 0 deletions packages/playground/admin/app/pages/blocks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { Binding, PersistButton } from '../../lib/components/binding'
import { Slots } from '../../lib/components/slots'
import * as React from 'react'
import { EntitySubTree, EntityView, Field, HasOne, StaticRender } from '@contember/interface'
import { DefaultBlockRepeater } from '../../lib/components/block-repeater'
import { ImageField, InputField, RadioEnumField, TextareaField } from '../../lib/components/form'
import { UploadedImageView } from '../../lib/components/upload'
import { Block } from '@contember/react-block-repeater'
import { AlertOctagonIcon, ImageIcon, TextIcon } from 'lucide-react'
import { cn } from '../../lib/utils/cn'

export default () => <>
<Binding>
<Slots.Actions>
<PersistButton />
</Slots.Actions>
<EntitySubTree entity={'BlockList(unique=unique)'} setOnCreate={'(unique=unique)'}>
<DefaultBlockRepeater field={'blocks'} sortableBy="order" discriminationField="type">
<Block
name="text"
label={<><TextIcon /> Text</>}
form={<>
<InputField field={'title'} label={'Title'} />
<TextareaField field={'content'} label={'Content'} />
</>}
children={<>
<div className="flex">
<div className="w-64 space-y-2">
<h2 className="text-xl font-bold">
<Field field={'title'} />
</h2>
<p>
<Field field={'content'} />
</p>
</div>
</div>
</>
}
/>
<Block
name="image"
label={<><ImageIcon /> Image</>}
form={<>
<InputField field={'title'} label={'Title'} />
<ImageField baseField={'image'} urlField="url" label={'Image'} />
</>}
children={<>
<div className="flex">
<div className="flex flex-col gap-2">
<div className="space-y-2">
<h2 className="text-xl font-bold">
<Field field={'title'} />
</h2>
<div className="border">
<HasOne field="image">
<UploadedImageView urlField={'url'} />
</HasOne>
</div>

</div>
</div>
</div>
</>}
/>
<Block
name="textWithImage"
label={<><span className="inline-flex gap-1"><ImageIcon /> <TextIcon /></span> Image with text</>}
form={<>
<InputField field={'title'} label={'Title'} />
<TextareaField field={'content'} label={'Content'} />
<ImageField baseField={'image'} urlField="url" label={'Image'} />
<RadioEnumField field={'imagePosition'} options={{ left: 'Left', right: 'Right' }} />
</>}
children={<>

<EntityView render={it => {
return (
<div className="flex">
<div className={cn('border', it.getField('imagePosition').value === 'right' ? 'order-2' : '')}>
<HasOne field="image">
<UploadedImageView urlField={'url'} />
</HasOne>
</div>
<div className="w-64 px-4 space-y-2">
<h2 className="text-xl font-bold">
<Field field={'title'} />
</h2>
<p>
<Field field={'content'} />
</p>
</div>
</div>
)
}} />
</>}
/>
<Block
name="hero"
label={<><AlertOctagonIcon /> Hero</>}
form={<>
<InputField field={'title'} label={'Title'} />
<TextareaField field={'content'} label={'Content'} />
<InputField field={'color'} label={'Color'} inputProps={{ type: 'color' }} />
</>}
children={<>
<StaticRender>
<Field field={'color'} />
</StaticRender>
<EntityView render={it => {
return (
<div className="flex">
<div className="w-96 p-4 gap-2 flex flex-col items-center" style={{
backgroundColor: it.getField<string>('color').value ?? undefined,
color: getTextColor(it.getField<string>('color').value ?? ''),
}}>
<h2 className="text-4xl font-bold">
<Field field={'title'} />
</h2>
<p className="text-xl">
<Field field={'content'} />
</p>
</div>
</div>
)
}} />
</>}
/>
</DefaultBlockRepeater>
</EntitySubTree>
</Binding>
</>

function getTextColor(backgroundColor: string) {
if (!backgroundColor) {
return 'black'
}
// Extract RGB values from a color in hex format
const r = parseInt(backgroundColor.slice(1, 3), 16)
const g = parseInt(backgroundColor.slice(3, 5), 16)
const b = parseInt(backgroundColor.slice(5, 7), 16)

// Calculate the luminance
const luminance = 0.2126 * (r / 255) ** 2.2 +
0.7152 * (g / 255) ** 2.2 +
0.0722 * (b / 255) ** 2.2

// Use a luminance threshold of 0.179 to decide on text color
return luminance > 0.179 ? 'black' : 'white'
}


export const withoutDualRender = () => <>
<Binding>
<Slots.Actions>
<PersistButton />
</Slots.Actions>
<EntitySubTree entity={'BlockList(unique=unique)'} setOnCreate={'(unique=unique)'}>
<DefaultBlockRepeater field={'blocks'} sortableBy="order" discriminationField="type">
<Block
name="text"
label={<><TextIcon /> Text</>}
>
<InputField field={'title'} label={'Title'} />
<TextareaField field={'content'} label={'Content'} />
</Block>
<Block
name="image"
label={<><ImageIcon /> Image</>}
>
<InputField field={'title'} label={'Title'} />
<ImageField baseField={'image'} urlField="url" label={'Image'} />
</Block>
<Block
name="textWithImage"
label={<><span className="inline-flex gap-1"><ImageIcon /> <TextIcon /></span> Image with text</>}
>
<InputField field={'title'} label={'Title'} />
<TextareaField field={'content'} label={'Content'} />
<ImageField baseField={'image'} urlField="url" label={'Image'} />
<RadioEnumField field={'imagePosition'} options={{ left: 'Left', right: 'Right' }} />
</Block>
<Block
name="hero"
label={<><AlertOctagonIcon /> Hero</>}
>
<InputField field={'title'} label={'Title'} />
<TextareaField field={'content'} label={'Content'} />
<InputField field={'color'} label={'Color'} inputProps={{ type: 'color' }} />
</Block>
</DefaultBlockRepeater>
</EntitySubTree>
</Binding>
</>
5 changes: 5 additions & 0 deletions packages/playground/admin/app/pages/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as React from 'react'
import { Button } from '../../lib/components/ui/button'
import { Binding, PersistButton } from '../../lib/components/binding'
import { SelectOrTypeField } from '../../lib-extra/select-or-type-field'
import { FieldExists } from '../../lib-extra/has-field'


export const basic = () => <>
Expand All @@ -19,6 +20,10 @@ export const basic = () => <>
<InputField field={'floatValue'} label={'Float'} />
<InputField field={'dateValue'} label={'Date'} />
<InputField field={'datetimeValue'} label={'Date time'} />

<FieldExists field={'nonExistingField'}>
<InputField field={'nonExistingField'} label={'Date time'} />
</FieldExists>
</div>
</EntitySubTree>
</Binding>
Expand Down
Loading

0 comments on commit 9918ebe

Please sign in to comment.