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

block repeater #692

Merged
merged 11 commits into from
Apr 22, 2024
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
Loading