diff --git a/build/api/interface.api.md b/build/api/interface.api.md index ec21ca5ab..913664778 100644 --- a/build/api/interface.api.md +++ b/build/api/interface.api.md @@ -57,7 +57,7 @@ export interface DeleteEntityTriggerProps { // (undocumented) children: ReactNode; // (undocumented) - immediatePersist?: true; + immediatePersist?: boolean; // (undocumented) onPersistError?: (result: ErrorPersistResult) => void; // (undocumented) diff --git a/build/api/react-block-repeater.api.md b/build/api/react-block-repeater.api.md new file mode 100644 index 000000000..5e78c9245 --- /dev/null +++ b/build/api/react-block-repeater.api.md @@ -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; + +// @public (undocumented) +export interface BlockProps { + // (undocumented) + children: ReactNode; + // (undocumented) + form?: ReactNode; + // (undocumented) + label?: ReactNode; + // (undocumented) + name: string; +} + +// @public (undocumented) +export const BlockRepeater: NamedExoticComponent; + +// @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; + +// @public (undocumented) +export const useBlockRepeaterConfig: () => { + discriminatedBy: SugaredRelativeSingleField['field']; + blocks: BlocksMap; +}; + +// @public (undocumented) +export const useBlockRepeaterCurrentBlock: () => BlockProps | undefined; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/build/api/react-repeater.api.md b/build/api/react-repeater.api.md index fc3d1d0ff..415e2ae97 100644 --- a/build/api/react-repeater.api.md +++ b/build/api/react-repeater.api.md @@ -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; diff --git a/ee/admin-server/Dockerfile b/ee/admin-server/Dockerfile index da0149d8b..ccc6f0e72 100644 --- a/ee/admin-server/Dockerfile +++ b/ee/admin-server/Dockerfile @@ -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 diff --git a/packages/interface/src/components/binding/DeleteEntityTrigger.tsx b/packages/interface/src/components/binding/DeleteEntityTrigger.tsx index 57c37c45f..34ec2bf33 100644 --- a/packages/interface/src/components/binding/DeleteEntityTrigger.tsx +++ b/packages/interface/src/components/binding/DeleteEntityTrigger.tsx @@ -6,7 +6,7 @@ import { ErrorPersistResult, SuccessfulPersistResult, useEntity, useMutationStat const SlotButton = Slot as ComponentType> export interface DeleteEntityTriggerProps { - immediatePersist?: true + immediatePersist?: boolean children: ReactNode onPersistSuccess?: (result: SuccessfulPersistResult) => void onPersistError?: (result: ErrorPersistResult) => void diff --git a/packages/playground/admin/app/components/navigation.tsx b/packages/playground/admin/app/components/navigation.tsx index 33906e61b..70d741023 100644 --- a/packages/playground/admin/app/components/navigation.tsx +++ b/packages/playground/admin/app/components/navigation.tsx @@ -16,7 +16,12 @@ export const Navigation = () => { - } label={'Repeater'} to={'repeater'} /> + } label={'Repeater'}> + + + + + } label={'Grid'}> @@ -35,6 +40,14 @@ export const Navigation = () => { + } label={'Upload'}> + + + + + + + } label={'Dimensions'} to={'dimensions'} /> diff --git a/packages/playground/admin/app/pages/blocks.tsx b/packages/playground/admin/app/pages/blocks.tsx new file mode 100644 index 000000000..2e1b006f3 --- /dev/null +++ b/packages/playground/admin/app/pages/blocks.tsx @@ -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 () => <> + + + + + + + Text} + form={<> + + + } + children={<> +
+
+

+ +

+

+ +

+
+
+ + } + /> + Image} + form={<> + + + } + children={<> +
+
+
+

+ +

+
+ + + +
+ +
+
+
+ } + /> + Image with text} + form={<> + + + + + } + children={<> + + { + return ( +
+
+ + + +
+
+

+ +

+

+ +

+
+
+ ) + }} /> + } + /> + Hero} + form={<> + + + + } + children={<> + + + + { + return ( +
+
('color').value ?? undefined, + color: getTextColor(it.getField('color').value ?? ''), + }}> +

+ +

+

+ +

+
+
+ ) + }} /> + } + /> +
+
+
+ + +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 = () => <> + + + + + + + Text} + > + + + + Image} + > + + + + Image with text} + > + + + + + + Hero} + > + + + + + + + + diff --git a/packages/playground/admin/app/pages/input.tsx b/packages/playground/admin/app/pages/input.tsx index 44b4c6631..af0b328f1 100644 --- a/packages/playground/admin/app/pages/input.tsx +++ b/packages/playground/admin/app/pages/input.tsx @@ -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 = () => <> @@ -19,6 +20,10 @@ export const basic = () => <> + + + + diff --git a/packages/playground/admin/app/pages/repeater.tsx b/packages/playground/admin/app/pages/repeater.tsx index d50252068..65d33c94a 100644 --- a/packages/playground/admin/app/pages/repeater.tsx +++ b/packages/playground/admin/app/pages/repeater.tsx @@ -2,30 +2,50 @@ import * as React from 'react' import { Binding, DeleteEntityDialog, PersistButton } from '../../lib/components/binding' import { Slots } from '../../lib/components/slots' import { Field } from '@contember/interface' -import { DefaultRepeater } from '../../lib/components/repeater' +import { DefaultRepeater, RepeaterItemActions, RepeaterRemoveItemButton } from '../../lib/components/repeater' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, DropDownTriggerButton } from '../../lib/components/ui/dropdown' +const repeaterDropdown = ( + + + + + + Edit + Make a copy + + e.preventDefault()}> + Delete + } /> + + +) + export default <> - + + + + + {repeaterDropdown} + + + + + + + +export const nonSortable = <> + + + -
- - - - - - Edit - Make a copy - - e.preventDefault()}> - Delete - } /> - - -
+ + {repeaterDropdown} + +
diff --git a/packages/playground/admin/lib-extra/has-field.tsx b/packages/playground/admin/lib-extra/has-field.tsx new file mode 100644 index 000000000..9100654f3 --- /dev/null +++ b/packages/playground/admin/lib-extra/has-field.tsx @@ -0,0 +1,17 @@ +import { Component, SugaredRelativeSingleField } from '@contember/interface' +import { ReactNode } from 'react' +import { TreeNodeEnvironmentFactory } from '@contember/react-binding' + +export const FieldExists = Component<{ children: ReactNode, field: SugaredRelativeSingleField['field'] }>(({ field, children }, env) => { + try { + TreeNodeEnvironmentFactory.createEnvironmentForField(env, { field }) + } catch (e: any) { + if (import.meta.env.DEV) { + console.warn(e) + return
Field does not exist: {e.message}
+ } + return null + } + + return children +}) diff --git a/packages/playground/admin/lib/components/binding/delete.tsx b/packages/playground/admin/lib/components/binding/delete.tsx index fa0b28fe1..1a0afaff4 100644 --- a/packages/playground/admin/lib/components/binding/delete.tsx +++ b/packages/playground/admin/lib/components/binding/delete.tsx @@ -16,7 +16,7 @@ import { DeleteEntityTrigger } from '@contember/interface' import { FeedbackTrigger } from './persist' import { dict } from '../../dict' -export const DeleteEntityDialog = ({ trigger }: { trigger: ReactElement }) => { +export const DeleteEntityDialog = ({ trigger, immediatePersist }: { trigger: ReactElement, immediatePersist?: boolean }) => { return ( @@ -30,7 +30,7 @@ export const DeleteEntityDialog = ({ trigger }: { trigger: ReactElement }) => { {dict.deleteEntityDialog.cancelButton} - + diff --git a/packages/playground/admin/lib/components/binding/persist.tsx b/packages/playground/admin/lib/components/binding/persist.tsx index 0a86ab378..4b02244c8 100644 --- a/packages/playground/admin/lib/components/binding/persist.tsx +++ b/packages/playground/admin/lib/components/binding/persist.tsx @@ -87,7 +87,7 @@ export const usePersistWithFeedback = () => { const triggerPersist = usePersist() const { onPersistSuccess, onPersistError } = usePersistFeedbackHandlers() return useCallback(() => { - triggerPersist() + return triggerPersist() .then(onPersistSuccess) .catch(onPersistError) }, [onPersistError, onPersistSuccess, triggerPersist]) diff --git a/packages/playground/admin/lib/components/block-repeater.tsx b/packages/playground/admin/lib/components/block-repeater.tsx new file mode 100644 index 000000000..04d9f05bf --- /dev/null +++ b/packages/playground/admin/lib/components/block-repeater.tsx @@ -0,0 +1,194 @@ +import { Component, DeleteEntityTrigger, PersistTrigger, StaticRender, useEntity } from '@contember/interface' +import { RepeaterSortable, RepeaterSortableDragOverlay, RepeaterSortableEachItem, RepeaterSortableItemActivator, RepeaterSortableItemNode } from '@contember/react-repeater-dnd-kit' +import { GripVerticalIcon, TrashIcon } from 'lucide-react' +import { Button } from './ui/button' +import { uic } from '../utils/uic' +import { Sheet, SheetClose, SheetContent, SheetFooter, SheetHeader, SheetTitle } from './ui/sheet' +import { BlockRepeater, BlockRepeaterAddItemTrigger, BlockRepeaterProps, useBlockRepeaterConfig, useBlockRepeaterCurrentBlock } from '@contember/react-block-repeater' +import { RepeaterDropIndicator, RepeaterRemoveItemButton } from './repeater' +import { RepeaterEmpty } from '@contember/react-repeater' +import { dict } from '../dict' +import { FeedbackTrigger } from './binding' +import { createRequiredContext } from '@contember/react-utils' +import { useId, useState } from 'react' +import { Label } from './ui/label' +import { Switch } from './ui/switch' + +export const BlockRepeaterItemsWrapperUI = uic('div', { + baseClass: 'rounded border border-gray-300 p-4 flex flex-col', +}) +export const BlockRepeaterItemUI = uic('div', { + baseClass: ' relative border-b transition-all group', +}) +export const BlockRepeaterDragOverlayUI = uic('div', { + baseClass: 'rounded border border-gray-300 p-4 relative bg-opacity-60 bg-gray-100 backdrop-blur-sm', +}) +export const BlockRepeaterHandleUI = uic('button', { + baseClass: 'absolute top-1/2 -left-6 h-6 w-6 flex justify-end align-center opacity-10 hover:opacity-100 transition-opacity -translate-y-1/2', + beforeChildren: , +}) + +export const BlockRepeaterItemActions = uic('div', { + baseClass: 'absolute top-1 right-2 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity', +}) + +export type DefaultBlockRepeaterProps = + & BlockRepeaterProps + +const [BlockRepeaterEditModeContext, useBlockRepeaterEditMode] = createRequiredContext('BlockRepeaterEditMode') + +export const DefaultBlockRepeater = Component(({ children, ...props }) => { + const [editMode, setEditMode] = useState(false) + return ( + + + {children} + + + + +
+ +
+
+
+ ) +}, props => { + return +}) + +const ToggleEditMode = ({ setEditMode, editMode }: { setEditMode: (value: boolean) => void, editMode: boolean }) => { + const id = useId() + const { blocks } = useBlockRepeaterConfig() + const anyHasForm = Object.values(blocks).some(it => it.form) + if (!anyHasForm) { + return null + } + return ( +
+ + +
+ ) +} + +export const BlockRepeaterSortable = Component(() => { + return ( + + + +
+ {dict.repeater.empty} +
+
+ +
+ + + + + + + + + + +
+
+ + + + + + + +
+
+ ) +}) + +export const BlockRepeaterAddButtons = () => { + const { blocks } = useBlockRepeaterConfig() + return ( +
+ {Object.values(blocks).map(it => ( + + + + ))} +
+ ) +} + + +export const BlockRepeaterContent = () => { + const entity = useEntity() + const block = useBlockRepeaterCurrentBlock() + const children = block?.children + const editMode = useBlockRepeaterEditMode() + const editForm = block?.form ?? block?.children + const [editEntity, setEditEntity] = useState(!entity.existsOnServer) + + if (!block?.form || editMode) { + return ( +
+ + + + {editForm} +
+ ) + } + return <> +
setEditEntity(true)}> + {children} +
+ + +} + + +const BlockRepeaterEditSheetInner = ({ open, setOpen }: { open: boolean, setOpen: (value: boolean) => void }) => { + const block = useBlockRepeaterCurrentBlock() + const form = block?.form + const editMode = useBlockRepeaterEditMode() + + if (!form || editMode) { + return null + } + return ( + + { + e.preventDefault() + }}> + + + {block.label} + + + + + + + +
+ {form} +
+ + + + + + + + + + + + +
+
+ ) +} diff --git a/packages/playground/admin/lib/components/layout.tsx b/packages/playground/admin/lib/components/layout.tsx index 4d43d44a2..42c6c39e5 100644 --- a/packages/playground/admin/lib/components/layout.tsx +++ b/packages/playground/admin/lib/components/layout.tsx @@ -1,4 +1,4 @@ -import { LogOutIcon, MenuIcon, PanelLeftCloseIcon, PanelLeftOpenIcon, PanelRightCloseIcon, PanelRightOpenIcon } from 'lucide-react' +import { LogOutIcon, Maximize2Icon, MenuIcon, Minimize2Icon, PanelLeftCloseIcon, PanelLeftOpenIcon, PanelRightCloseIcon, PanelRightOpenIcon } from 'lucide-react' import { PropsWithChildren, useEffect, useState } from 'react' import { ComponentClassNameProps } from '@contember/utilities' import { useHasActiveSlotsFactory } from '@contember/react-slots' @@ -10,7 +10,18 @@ import { dict } from '../dict' import { useCurrentRequest } from '@contember/interface' const LayoutBodyUI = uic('div', { baseClass: 'bg-gray-50 h-full min-h-screen relative py-4 pl-[calc(100vw-100%)]' }) -const LayoutMaxWidthUI = uic('div', { baseClass: 'max-w-[100rem] mx-auto' }) +const LayoutMaxWidthUI = uic('div', { + baseClass: 'mx-auto transition-all', + variants: { + layout: { + stretch: 'max-w-[calc(100vw-5rem)]', + default: 'max-w-[100rem]', + }, + }, + defaultVariants: { + layout: 'default', + }, +}) const LayoutBoxUI = uic('div', { baseClass: 'rounded-xl shadow-lg border bg-white gap-1 flex flex-col lg:flex-row mt-4 relative min-h-[calc(100vh-10rem)]' }) const LayoutCenterPanelUI = uic('div', { baseClass: 'flex flex-col flex-2 p-4 gap-2 w-full flex-auto overflow-hidden' }) @@ -50,12 +61,14 @@ const LayoutLeftPanelCloserUI = uic('a', { baseClass: 'hidden lg:flex self-end a const LayoutLeftPanelOpenerUI = uic('a', { baseClass: 'hidden lg:block absolute top-1 left-1' }) const LayoutRightPanelCloserUI = uic('a', { baseClass: 'hidden lg:flex self-end absolute top-1 right-1 opacity-0 text-gray-400 hover:opacity-100 transition-opacity cursor-pointer' }) const LayoutRightPanelOpenerUI = uic('a', { baseClass: 'absolute top-1 right-1' }) +const LayoutSwitcherUI = uic('a', { baseClass: 'hidden lg:flex self-end absolute top-1 right-1 opacity-20 text-gray-400 hover:opacity-100 transition-opacity cursor-pointer' }) export const LayoutComponent = ({ children, ...rest }: PropsWithChildren) => { const isActive = useHasActiveSlotsFactory() const [leftSidebarVisibility, setLeftSidebarVisibility] = useState<'show' | 'hidden' | 'auto'>('auto') + const [layout, setLayout] = useState<'default' | 'stretch'>('default') const request = useCurrentRequest() useEffect(() => { @@ -67,7 +80,12 @@ export const LayoutComponent = ({ children, ...rest }: PropsWithChildren - + setLayout(it => it === 'default' ? 'stretch' : 'default')}> + {layout === 'default' ? : } + + + + {leftSidebarVisibility === 'hidden' && ( setLeftSidebarVisibility('auto')}> diff --git a/packages/playground/admin/lib/components/repeater.tsx b/packages/playground/admin/lib/components/repeater.tsx index 1266be336..fb0238ee8 100644 --- a/packages/playground/admin/lib/components/repeater.tsx +++ b/packages/playground/admin/lib/components/repeater.tsx @@ -3,6 +3,7 @@ import { Repeater, RepeaterAddItemTrigger, RepeaterEachItem, + RepeaterEmpty, RepeaterProps, RepeaterRemoveItemTrigger, } from '@contember/react-repeater' @@ -18,11 +19,9 @@ import { GripVerticalIcon, PlusCircleIcon, Trash2Icon } from 'lucide-react' import { Button } from './ui/button' import { uic } from '../utils/uic' import { DropIndicator } from './ui/sortable' +import { dict } from '../dict' export const RepeaterWrapperUI = uic('div', { - baseClass: 'flex flex-col gap-2', -}) -export const RepeaterItemsWrapperUI = uic('div', { baseClass: 'flex flex-col gap-2 p-4 pr-8 relative shadow-sm bg-white rounded border border-gray-300 max-w-md', }) export const RepeaterItemUI = uic('div', { @@ -59,16 +58,18 @@ export const RepeaterAddItemButton = ({ children }: { children?: React.ReactNode export const RepeaterRemoveItemButton = ({ children }: { children?: React.ReactNode }) => ( -
- -
+
) +export const RepeaterItemActions = uic('div', { + baseClass: 'absolute top-1 right-2 flex gap-2', +}) + export type DefaultRepeaterProps = { title?: string } & RepeaterProps @@ -82,11 +83,15 @@ export const DefaultRepeater = Component(({ title, childre {title &&

{title}

} + +
+ {dict.repeater.empty} +
+
- - + {children} - +
@@ -98,9 +103,14 @@ export const DefaultRepeater = Component(({ title, childre return (
- + {title &&

{title}

} + +
+ {dict.repeater.empty} +
+
@@ -122,8 +132,8 @@ export const DefaultRepeater = Component(({ title, childre - - + +
) diff --git a/packages/playground/admin/lib/components/ui/sheet.tsx b/packages/playground/admin/lib/components/ui/sheet.tsx new file mode 100644 index 000000000..30e7e7952 --- /dev/null +++ b/packages/playground/admin/lib/components/ui/sheet.tsx @@ -0,0 +1,67 @@ +'use client' + +import * as React from 'react' +import * as SheetPrimitive from '@radix-ui/react-dialog' +import { uic } from '../../utils/uic' +import { XIcon } from 'lucide-react' + +export const Sheet = SheetPrimitive.Root + +export const SheetTrigger = SheetPrimitive.Trigger + +export const SheetClose = SheetPrimitive.Close + +export const SheetPortal = SheetPrimitive.Portal + +export const SheetOverlay = uic(SheetPrimitive.Overlay, { + baseClass: 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', + displayName: 'SheetOverlay', +}) + +export const SheetContent = uic(SheetPrimitive.Content, { + baseClass: 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500', + variants: { + side: { + top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', + bottom: 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', + left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', + right: 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', + }, + }, + defaultVariants: { + side: 'right', + }, + displayName: 'SheetContent', + wrapOuter: props => ( + + {/**/} + {props.children} + + + Close + + + ), +}) + + +export const SheetHeader = uic('div', { + baseClass: 'flex flex-col space-y-2 text-center sm:text-left', + displayName: 'SheetHeader', +}) + + +export const SheetFooter = uic('div', { + baseClass: 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', + displayName: 'SheetFooter', +}) + +export const SheetTitle = uic(SheetPrimitive.Title, { + baseClass: 'text-lg font-semibold text-foreground', + displayName: 'SheetTitle', +}) + +export const SheetDescription = uic(SheetPrimitive.Description, { + baseClass: 'text-sm text-muted-foreground', + displayName: 'SheetDescription', +}) diff --git a/packages/playground/admin/lib/components/ui/switch.tsx b/packages/playground/admin/lib/components/ui/switch.tsx new file mode 100644 index 000000000..4a89d1cc1 --- /dev/null +++ b/packages/playground/admin/lib/components/ui/switch.tsx @@ -0,0 +1,10 @@ +import * as React from 'react' +import * as SwitchPrimitives from '@radix-ui/react-switch' +import { uic } from '../../utils/uic' + +export const Switch = uic(SwitchPrimitives.Root, { + baseClass: 'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input', + afterChildren: , +}) diff --git a/packages/playground/admin/lib/components/upload/view.tsx b/packages/playground/admin/lib/components/upload/view.tsx index 5e95784c0..cb276d643 100644 --- a/packages/playground/admin/lib/components/upload/view.tsx +++ b/packages/playground/admin/lib/components/upload/view.tsx @@ -19,10 +19,10 @@ export type UploadedImageViewProps = export const UploadedImageView = Component(({ DestroyAction, ...props }) => { const url = useField(props.urlField).value return ( -
+
{url && } - +
) @@ -199,7 +199,7 @@ const FileActions = ({ DestroyAction, children }: {
- diff --git a/packages/playground/admin/lib/dict.ts b/packages/playground/admin/lib/dict.ts index a374009de..cfed3fcf1 100644 --- a/packages/playground/admin/lib/dict.ts +++ b/packages/playground/admin/lib/dict.ts @@ -108,4 +108,8 @@ export const dict = { dropFiles: 'Drop files here', or: 'or', }, + repeater: { + empty: 'No items.', + addItem: 'Add item', + }, } diff --git a/packages/playground/api/client/entities.ts b/packages/playground/api/client/entities.ts index 7968b94bf..d91b6824f 100644 --- a/packages/playground/api/client/entities.ts +++ b/packages/playground/api/client/entities.ts @@ -1,9 +1,12 @@ +import type { BlockType } from './enums' import type { BoardTaskStatus } from './enums' import type { GridArticleState } from './enums' import type { InputUnique } from './enums' import type { SelectUnique } from './enums' import type { UploadMediaType } from './enums' import type { UploadOne } from './enums' +import type { BlockImagePosition } from './enums' +import type { BlockListUnique } from './enums' import type { DimensionsItemUnique } from './enums' import type { InputRootEnumValue } from './enums' @@ -12,6 +15,63 @@ export type JSONValue = JSONPrimitive | JSONObject | JSONArray export type JSONObject = { readonly [K in string]?: JSONValue } export type JSONArray = readonly JSONValue[] +export type Block = { + name: 'Block' + unique: + | Omit<{ id: string}, OverRelation> + | Omit<{ image: BlockImage['unique']}, OverRelation> + columns: { + id: string + order: number + type: BlockType + title: string + content: string | null + imagePosition: BlockImagePosition | null + color: string | null + } + hasOne: { + list: BlockList + image: BlockImage + } + hasMany: { + } + hasManyBy: { + } +} +export type BlockImage = { + name: 'BlockImage' + unique: + | Omit<{ id: string}, OverRelation> + columns: { + id: string + url: string | null + } + hasOne: { + } + hasMany: { + } + hasManyBy: { + } +} +export type BlockList = { + name: 'BlockList' + unique: + | Omit<{ id: string}, OverRelation> + | Omit<{ unique: BlockListUnique}, OverRelation> + | Omit<{ blocks: Block['unique']}, OverRelation> + columns: { + id: string + unique: BlockListUnique + } + hasOne: { + } + hasMany: { + blocks: Block<'list'> + } + hasManyBy: { + blocksByImage: { entity: Block; by: {image: BlockImage['unique']} } + } +} export type BoardTag = { name: 'BoardTag' unique: @@ -599,6 +659,9 @@ export type UploadVideo = { } export type ContemberClientEntities = { + Block: Block + BlockImage: BlockImage + BlockList: BlockList BoardTag: BoardTag BoardTask: BoardTask BoardUser: BoardUser diff --git a/packages/playground/api/client/enums.ts b/packages/playground/api/client/enums.ts index 9a9d36caf..f7eda6c14 100644 --- a/packages/playground/api/client/enums.ts +++ b/packages/playground/api/client/enums.ts @@ -1,3 +1,8 @@ +export type BlockType = + | "text" + | "image" + | "textWithImage" + | "hero" export type BoardTaskStatus = | "backlog" | "todo" @@ -18,6 +23,11 @@ export type UploadMediaType = | "file" export type UploadOne = | "unique" +export type BlockImagePosition = + | "left" + | "right" +export type BlockListUnique = + | "unique" export type DimensionsItemUnique = | "unique" export type InputRootEnumValue = diff --git a/packages/playground/api/client/names.ts b/packages/playground/api/client/names.ts index d393889f9..5f4364956 100644 --- a/packages/playground/api/client/names.ts +++ b/packages/playground/api/client/names.ts @@ -1,6 +1,83 @@ import { SchemaNames } from '@contember/client-content' export const ContemberClientNames: SchemaNames = { "entities": { + "Block": { + "name": "Block", + "fields": { + "id": { + "type": "column" + }, + "list": { + "type": "one", + "entity": "BlockList" + }, + "order": { + "type": "column" + }, + "type": { + "type": "column" + }, + "title": { + "type": "column" + }, + "content": { + "type": "column" + }, + "image": { + "type": "one", + "entity": "BlockImage" + }, + "imagePosition": { + "type": "column" + }, + "color": { + "type": "column" + } + }, + "scalars": [ + "id", + "order", + "type", + "title", + "content", + "imagePosition", + "color" + ] + }, + "BlockImage": { + "name": "BlockImage", + "fields": { + "id": { + "type": "column" + }, + "url": { + "type": "column" + } + }, + "scalars": [ + "id", + "url" + ] + }, + "BlockList": { + "name": "BlockList", + "fields": { + "id": { + "type": "column" + }, + "unique": { + "type": "column" + }, + "blocks": { + "type": "many", + "entity": "Block" + } + }, + "scalars": [ + "id", + "unique" + ] + }, "BoardTag": { "name": "BoardTag", "fields": { diff --git a/packages/playground/api/migrations/2024-04-19-130834-blocks.json b/packages/playground/api/migrations/2024-04-19-130834-blocks.json new file mode 100644 index 000000000..d3109aa98 --- /dev/null +++ b/packages/playground/api/migrations/2024-04-19-130834-blocks.json @@ -0,0 +1,241 @@ +{ + "formatVersion": 5, + "modifications": [ + { + "modification": "createEnum", + "enumName": "BlockType", + "values": [ + "text", + "image", + "textWithImage", + "hero" + ] + }, + { + "modification": "createEnum", + "enumName": "BlockImagePosition", + "values": [ + "left", + "right" + ] + }, + { + "modification": "createEnum", + "enumName": "BlockListUnique", + "values": [ + "unique" + ] + }, + { + "modification": "createEntity", + "entity": { + "name": "Block", + "primary": "id", + "primaryColumn": "id", + "tableName": "block", + "fields": { + "id": { + "name": "id", + "columnName": "id", + "columnType": "uuid", + "nullable": false, + "type": "Uuid" + } + }, + "unique": [], + "indexes": [], + "eventLog": { + "enabled": true + } + } + }, + { + "modification": "createEntity", + "entity": { + "name": "BlockImage", + "primary": "id", + "primaryColumn": "id", + "tableName": "block_image", + "fields": { + "id": { + "name": "id", + "columnName": "id", + "columnType": "uuid", + "nullable": false, + "type": "Uuid" + } + }, + "unique": [], + "indexes": [], + "eventLog": { + "enabled": true + } + } + }, + { + "modification": "createEntity", + "entity": { + "name": "BlockList", + "primary": "id", + "primaryColumn": "id", + "tableName": "block_list", + "fields": { + "id": { + "name": "id", + "columnName": "id", + "columnType": "uuid", + "nullable": false, + "type": "Uuid" + } + }, + "unique": [], + "indexes": [], + "eventLog": { + "enabled": true + } + } + }, + { + "modification": "createColumn", + "entityName": "Block", + "field": { + "name": "order", + "columnName": "order", + "columnType": "integer", + "nullable": false, + "type": "Integer" + } + }, + { + "modification": "createColumn", + "entityName": "Block", + "field": { + "name": "type", + "columnName": "type", + "columnType": "BlockType", + "nullable": false, + "type": "Enum" + } + }, + { + "modification": "createColumn", + "entityName": "Block", + "field": { + "name": "title", + "columnName": "title", + "columnType": "text", + "nullable": false, + "type": "String" + } + }, + { + "modification": "createColumn", + "entityName": "Block", + "field": { + "name": "content", + "columnName": "content", + "columnType": "text", + "nullable": true, + "type": "String" + } + }, + { + "modification": "createColumn", + "entityName": "Block", + "field": { + "name": "imagePosition", + "columnName": "image_position", + "columnType": "BlockImagePosition", + "nullable": true, + "type": "Enum" + } + }, + { + "modification": "createColumn", + "entityName": "Block", + "field": { + "name": "color", + "columnName": "color", + "columnType": "text", + "nullable": true, + "type": "String" + } + }, + { + "modification": "createColumn", + "entityName": "BlockImage", + "field": { + "name": "url", + "columnName": "url", + "columnType": "text", + "nullable": true, + "type": "String" + } + }, + { + "modification": "createColumn", + "entityName": "BlockList", + "field": { + "name": "unique", + "columnName": "unique", + "columnType": "BlockListUnique", + "nullable": false, + "type": "Enum", + "default": "unique" + }, + "fillValue": "unique" + }, + { + "modification": "createRelation", + "entityName": "Block", + "owningSide": { + "type": "ManyHasOne", + "name": "list", + "target": "BlockList", + "joiningColumn": { + "columnName": "list_id", + "onDelete": "restrict" + }, + "nullable": true, + "inversedBy": "blocks" + }, + "inverseSide": { + "type": "OneHasMany", + "name": "blocks", + "target": "Block", + "ownedBy": "list", + "orderBy": [ + { + "path": [ + "order" + ], + "direction": "asc" + } + ] + } + }, + { + "modification": "createRelation", + "entityName": "Block", + "owningSide": { + "type": "OneHasOne", + "name": "image", + "target": "BlockImage", + "joiningColumn": { + "columnName": "image_id", + "onDelete": "restrict" + }, + "nullable": true + } + }, + { + "modification": "createUniqueConstraint", + "entityName": "BlockList", + "unique": { + "fields": [ + "unique" + ] + } + } + ] +} diff --git a/packages/playground/api/migrations/2024-04-19-130909-blocks-data.ts b/packages/playground/api/migrations/2024-04-19-130909-blocks-data.ts new file mode 100644 index 000000000..03fb2f203 --- /dev/null +++ b/packages/playground/api/migrations/2024-04-19-130909-blocks-data.ts @@ -0,0 +1,8 @@ +import { printMutation } from './utils' +import { queryBuilder } from '../client' + +export default printMutation([ + queryBuilder.create('BlockList', { + data: { unique: 'unique' }, + }), +]) diff --git a/packages/playground/api/model/Blocks.ts b/packages/playground/api/model/Blocks.ts new file mode 100644 index 000000000..db668d8e7 --- /dev/null +++ b/packages/playground/api/model/Blocks.ts @@ -0,0 +1,28 @@ +import { c } from '@contember/schema-definition' + +export class BlockList { + unique = c.enumColumn(c.createEnum('unique')).default('unique').notNull().unique() + blocks = c.oneHasMany(Block, 'list').orderBy('order') +} + +export const BlockType = c.createEnum( + 'text', // title, content + 'image', // title, image + 'textWithImage', // title, content, image, imagePosition + 'hero', // title, content, color +) + +export class Block { + list = c.manyHasOne(BlockList, 'blocks') + order = c.intColumn().notNull() + type = c.enumColumn(BlockType).notNull() + title = c.stringColumn().notNull() + content = c.stringColumn() + image = c.oneHasOne(BlockImage) + imagePosition = c.enumColumn(c.createEnum('left', 'right')) + color = c.stringColumn() +} + +export class BlockImage { + url = c.stringColumn() +} diff --git a/packages/playground/api/model/index.ts b/packages/playground/api/model/index.ts index b992efa4a..4fa13bf7c 100644 --- a/packages/playground/api/model/index.ts +++ b/packages/playground/api/model/index.ts @@ -1,3 +1,4 @@ +export * from './Blocks' export * from './Board' export * from './Dimensions' export * from './Grid' diff --git a/packages/playground/api/tsconfig.json b/packages/playground/api/tsconfig.json index eb94116ea..0a00d595e 100644 --- a/packages/playground/api/tsconfig.json +++ b/packages/playground/api/tsconfig.json @@ -6,7 +6,8 @@ "references": [ { "path": "../../interface/src" }, { "path": "../../react-board/src" }, - { "path": "../../react-board-dnd-kit/src" }, + { "path": "../../react-board/src" }, + { "path": "../../react-block-repeater/src" }, { "path": "../../react-repeater/src" }, { "path": "../../react-repeater-dnd-kit/src" }, { "path": "../../react-select/src" }, diff --git a/packages/playground/package.json b/packages/playground/package.json index efc8b350f..948f9bab9 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -16,6 +16,7 @@ "@contember/graphql-builder": "workspace:*", "@contember/interface": "workspace:*", "@contember/interface-tester": "workspace:*", + "@contember/react-block-repeater": "workspace:*", "@contember/react-board": "workspace:*", "@contember/react-board-dnd-kit": "workspace:*", "@contember/react-dataview": "workspace:*", @@ -44,6 +45,7 @@ "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", "autoprefixer": "^10", diff --git a/packages/react-block-repeater/api-extractor.json b/packages/react-block-repeater/api-extractor.json new file mode 100644 index 000000000..66c17dd71 --- /dev/null +++ b/packages/react-block-repeater/api-extractor.json @@ -0,0 +1,3 @@ +{ + "extends": "../../build/api-extractor.json" +} diff --git a/packages/react-block-repeater/package.json b/packages/react-block-repeater/package.json new file mode 100644 index 000000000..2b92a9b4b --- /dev/null +++ b/packages/react-block-repeater/package.json @@ -0,0 +1,57 @@ +{ + "name": "@contember/react-block-repeater", + "license": "Apache-2.0", + "version": "0.0.0", + "type": "module", + "sideEffects": false, + "main": "./dist/production/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.js", + "production": "./dist/production/index.js", + "default": "./dist/production/index.js" + }, + "require": { + "types": "./dist/types/index.d.ts", + "development": "./dist/development/index.cjs", + "production": "./dist/production/index.cjs", + "default": "./dist/production/index.cjs" + } + } + }, + "files": [ + "dist/", + "src/" + ], + "typings": "./dist/types/index.d.ts", + "scripts": { + "build": "yarn build:js:dev && yarn build:js:prod", + "build:js:dev": "vite build --mode development", + "build:js:prod": "vite build --mode production", + "ae:build": "api-extractor run --local", + "ae:test": "api-extractor run", + "test": "vitest" + }, + "dependencies": { + "@contember/react-binding": "workspace:*", + "@contember/react-multipass-rendering": "workspace:*", + "@contember/react-repeater": "workspace:*", + "@contember/react-utils": "workspace:*", + "@radix-ui/react-slot": "^1.0.2" + }, + "peerDependencies": { + "react": "^17 || ^18", + "react-dom": "^17 || ^18" + }, + "devDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/contember/interface.git", + "directory": "packages/react-block-repeater" + } +} diff --git a/packages/react-block-repeater/src/components/Block.tsx b/packages/react-block-repeater/src/components/Block.tsx new file mode 100644 index 000000000..98af46e1f --- /dev/null +++ b/packages/react-block-repeater/src/components/Block.tsx @@ -0,0 +1,19 @@ +import { BindingError, Component } from '@contember/react-binding' +import { ReactNode } from 'react' + +export interface BlockProps { + children: ReactNode + name: string + label?: ReactNode + form?: ReactNode +} + +export const Block = Component(props => { + throw new BindingError('"Block" component is not supposed to be rendered.') +}, ({ label, children, form }) => { + return <> + {label} + {children} + {form} + +}) diff --git a/packages/react-block-repeater/src/components/BlockRepeater.tsx b/packages/react-block-repeater/src/components/BlockRepeater.tsx new file mode 100644 index 000000000..d6f3b90e1 --- /dev/null +++ b/packages/react-block-repeater/src/components/BlockRepeater.tsx @@ -0,0 +1,33 @@ +import { Component, Field, SugaredRelativeSingleField } from '@contember/react-binding' +import { Repeater, RepeaterProps } from '@contember/react-repeater' +import { useState } from 'react' +import { extractBlocks } from '../internal/helpers/staticAnalyzer' +import { BlockRepeaterConfigContext } from '../contexts' + +export type BlockRepeaterProps = + & { + sortableBy: RepeaterProps['sortableBy'] + discriminationField: SugaredRelativeSingleField['field'] + } + & RepeaterProps + +export const BlockRepeater = Component(({ children, ...props }, env) => { + const [blocks] = useState(() => extractBlocks(children, env)) + if (Object.keys(blocks).length === 0) { + throw new Error('BlockRepeater must have at least one Block child') + } + return ( + + + {children} + + + ) +}, props => { + return ( + + {props.children} + + + ) +}) diff --git a/packages/react-block-repeater/src/components/BlockRepeaterAddItemTrigger.tsx b/packages/react-block-repeater/src/components/BlockRepeaterAddItemTrigger.tsx new file mode 100644 index 000000000..b56736958 --- /dev/null +++ b/packages/react-block-repeater/src/components/BlockRepeaterAddItemTrigger.tsx @@ -0,0 +1,23 @@ +import { Slot } from '@radix-ui/react-slot' +import { useBlockRepeaterConfig } from '../contexts' +import React, { ReactElement, useMemo } from 'react' +import { EntityAccessor } from '@contember/react-binding' +import { RepeaterAddItemIndex, useRepeaterMethods } from '@contember/react-repeater' + +export interface BlockRepeaterAddItemTriggerProps { + children: ReactElement + type: string + index?: RepeaterAddItemIndex + preprocess?: EntityAccessor.BatchUpdatesHandler +} + +export const BlockRepeaterAddItemTrigger = ({ preprocess, index, type, ...props }: BlockRepeaterAddItemTriggerProps) => { + const { discriminatedBy } = useBlockRepeaterConfig() + + const { addItem } = useRepeaterMethods() + const onClick = useMemo(() => () => addItem?.(index, it => { + it().getField(discriminatedBy).updateValue(type) + }), [addItem, discriminatedBy, index, type]) + + return +} diff --git a/packages/react-block-repeater/src/components/index.ts b/packages/react-block-repeater/src/components/index.ts new file mode 100644 index 000000000..a8fcccdcb --- /dev/null +++ b/packages/react-block-repeater/src/components/index.ts @@ -0,0 +1,4 @@ +export * from './Block' +export * from './BlockRepeater' +export * from './BlockRepeaterAddItemTrigger' + diff --git a/packages/react-block-repeater/src/contexts.ts b/packages/react-block-repeater/src/contexts.ts new file mode 100644 index 000000000..2141086c4 --- /dev/null +++ b/packages/react-block-repeater/src/contexts.ts @@ -0,0 +1,11 @@ +import { SugaredRelativeSingleField } from '@contember/react-binding' +import { createRequiredContext } from '@contember/react-utils' +import { BlocksMap } from './types' + +const BlockRepeaterConfigContext_ = createRequiredContext<{ + discriminatedBy: SugaredRelativeSingleField['field'] + blocks: BlocksMap +}>('BlockRepeaterConfigContext') +/** @internal */ +export const BlockRepeaterConfigContext = BlockRepeaterConfigContext_[0] +export const useBlockRepeaterConfig = BlockRepeaterConfigContext_[1] diff --git a/packages/react-block-repeater/src/hooks/index.ts b/packages/react-block-repeater/src/hooks/index.ts new file mode 100644 index 000000000..1d6d7a1c7 --- /dev/null +++ b/packages/react-block-repeater/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './useBlockRepeaterCurrentBlock' diff --git a/packages/react-block-repeater/src/hooks/useBlockRepeaterCurrentBlock.ts b/packages/react-block-repeater/src/hooks/useBlockRepeaterCurrentBlock.ts new file mode 100644 index 000000000..89ed4c741 --- /dev/null +++ b/packages/react-block-repeater/src/hooks/useBlockRepeaterCurrentBlock.ts @@ -0,0 +1,9 @@ +import { useEntity } from '@contember/react-binding' +import { useBlockRepeaterConfig } from '../contexts' + +export const useBlockRepeaterCurrentBlock = () => { + const entity = useEntity() + const { discriminatedBy, blocks } = useBlockRepeaterConfig() + const field = entity.getField(discriminatedBy).value + return field ? blocks[field] : undefined +} diff --git a/packages/react-block-repeater/src/index.ts b/packages/react-block-repeater/src/index.ts new file mode 100644 index 000000000..1b3b6dfba --- /dev/null +++ b/packages/react-block-repeater/src/index.ts @@ -0,0 +1,4 @@ +export * from './contexts' +export * from './components' +export * from './hooks' +export * from './types' diff --git a/packages/react-block-repeater/src/internal/helpers/staticAnalyzer.ts b/packages/react-block-repeater/src/internal/helpers/staticAnalyzer.ts new file mode 100644 index 000000000..213d2b57b --- /dev/null +++ b/packages/react-block-repeater/src/internal/helpers/staticAnalyzer.ts @@ -0,0 +1,22 @@ +import { ReactNode } from 'react' +import { Block, BlockProps } from '../../components' +import { Environment } from '@contember/react-binding' +import { ChildrenAnalyzer, Leaf } from '@contember/react-multipass-rendering' + +type BlocksMap = Record +export const extractBlocks = (children: ReactNode, env: Environment): BlocksMap => { + const blocks = blockAnalyzer.processChildren(children, env) + return Object.fromEntries(blocks.map(block => [block.name, block])) +} + +const columnLeaf = new Leaf(node => node.props, Block) + +const blockAnalyzer = new ChildrenAnalyzer< + BlockProps, + never, + Environment +>([columnLeaf], { + staticRenderFactoryName: 'staticRender', + staticContextFactoryName: 'generateEnvironment', + unhandledNodeErrorMessage: 'Only Block children are supported.', +}) diff --git a/packages/react-block-repeater/src/tsconfig.json b/packages/react-block-repeater/src/tsconfig.json new file mode 100644 index 000000000..346e54a0d --- /dev/null +++ b/packages/react-block-repeater/src/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.settings.json", + "compilerOptions": { + "outDir": "../dist/types" + }, + "references": [ + { "path": "../../react-binding/src" }, + { "path": "../../react-multipass-rendering/src" }, + { "path": "../../react-repeater/src" }, + { "path": "../../react-utils/src" }, + ] +} diff --git a/packages/react-block-repeater/src/types/index.ts b/packages/react-block-repeater/src/types/index.ts new file mode 100644 index 000000000..3377bbb51 --- /dev/null +++ b/packages/react-block-repeater/src/types/index.ts @@ -0,0 +1,3 @@ +import { BlockProps } from '../components/Block' + +export type BlocksMap = Record diff --git a/packages/react-block-repeater/tests/example.test.ts b/packages/react-block-repeater/tests/example.test.ts new file mode 100644 index 000000000..7d9fbdfe2 --- /dev/null +++ b/packages/react-block-repeater/tests/example.test.ts @@ -0,0 +1,5 @@ +import { describe, test } from 'vitest' + +describe('@contember/react-block-repeater', function () { + test('@contember/react-block-repeater', function () { }) +}) diff --git a/packages/react-block-repeater/tests/tsconfig.json b/packages/react-block-repeater/tests/tsconfig.json new file mode 100644 index 000000000..9f0c88bc7 --- /dev/null +++ b/packages/react-block-repeater/tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.settings.json", + "compilerOptions": { + "noEmit": true, + "emitDeclarationOnly": false + }, + "references": [ + { "path": "../src" }, + ], +} diff --git a/packages/react-block-repeater/tsconfig.json b/packages/react-block-repeater/tsconfig.json new file mode 100644 index 000000000..915c57c02 --- /dev/null +++ b/packages/react-block-repeater/tsconfig.json @@ -0,0 +1,8 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "./src" }, + { "path": "./tests" }, + ], +} diff --git a/packages/react-block-repeater/tsdoc.json b/packages/react-block-repeater/tsdoc.json new file mode 100644 index 000000000..a46f62a20 --- /dev/null +++ b/packages/react-block-repeater/tsdoc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": [ + "../../tsdoc.json" + ] +} diff --git a/packages/react-block-repeater/vite.config.js b/packages/react-block-repeater/vite.config.js new file mode 100644 index 000000000..a9bd4c5d4 --- /dev/null +++ b/packages/react-block-repeater/vite.config.js @@ -0,0 +1,5 @@ +import { createViteConfig } from '../../build/createViteConfig.js' + +const currentDirName = new URL('.', import.meta.url).pathname.split('/').filter(Boolean).pop() + +export default createViteConfig(currentDirName) diff --git a/packages/react-repeater/src/components/Repeater.tsx b/packages/react-repeater/src/components/Repeater.tsx index bade74ffd..45f0bf787 100644 --- a/packages/react-repeater/src/components/Repeater.tsx +++ b/packages/react-repeater/src/components/Repeater.tsx @@ -60,7 +60,7 @@ const RepeaterRelative = Component( ) }, (props, environment) => ( - + {props.children} {props.sortableBy && } @@ -78,7 +78,7 @@ const RepeaterQualified = Component( ) }, (props, environment) => ( - + {props.children} {props.sortableBy && } diff --git a/packages/react-repeater/src/components/triggers/RepeaterAddItemTrigger.tsx b/packages/react-repeater/src/components/triggers/RepeaterAddItemTrigger.tsx index 2edfff847..49bd39654 100644 --- a/packages/react-repeater/src/components/triggers/RepeaterAddItemTrigger.tsx +++ b/packages/react-repeater/src/components/triggers/RepeaterAddItemTrigger.tsx @@ -2,10 +2,16 @@ import React, { ReactNode, useMemo } from 'react' import { RepeaterAddItemIndex } from '../../types/RepeaterMethods' import { useRepeaterMethods } from '../../contexts' import { Slot } from '@radix-ui/react-slot' +import { EntityAccessor } from '@contember/binding' -export const RepeaterAddItemTrigger = ({ children, index }: { children: ReactNode, index: RepeaterAddItemIndex }) => { +export type RepeaterAddItemTriggerProps = { + children: ReactNode + index?: RepeaterAddItemIndex + preprocess?: EntityAccessor.BatchUpdatesHandler +} +export const RepeaterAddItemTrigger = ({ children, index, preprocess }: RepeaterAddItemTriggerProps) => { const { addItem } = useRepeaterMethods() - const onClick = useMemo(() => () => addItem?.(index), [addItem, index]) + const onClick = useMemo(() => () => addItem?.(index, preprocess), [addItem, index, preprocess]) return {children} } diff --git a/tsconfig.json b/tsconfig.json index 14ef4b69a..5dfad78c2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -53,6 +53,9 @@ { "path": "./packages/react-binding-ui" }, + { + "path": "./packages/react-block-repeater" + }, { "path": "./packages/react-board" }, diff --git a/yarn.lock b/yarn.lock index 4ca6028c5..5602fedcc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1285,6 +1285,7 @@ __metadata: "@contember/graphql-builder": "workspace:*" "@contember/interface": "workspace:*" "@contember/interface-tester": "workspace:*" + "@contember/react-block-repeater": "workspace:*" "@contember/react-board": "workspace:*" "@contember/react-board-dnd-kit": "workspace:*" "@contember/react-dataview": "workspace:*" @@ -1310,6 +1311,7 @@ __metadata: "@radix-ui/react-scroll-area": ^1.0.5 "@radix-ui/react-select": ^2.0.0 "@radix-ui/react-slot": ^1.0.2 + "@radix-ui/react-switch": ^1.0.3 "@radix-ui/react-toast": ^1.1.5 "@radix-ui/react-tooltip": ^1.0.7 autoprefixer: ^10 @@ -1391,6 +1393,23 @@ __metadata: languageName: unknown linkType: soft +"@contember/react-block-repeater@workspace:*, @contember/react-block-repeater@workspace:packages/react-block-repeater": + version: 0.0.0-use.local + resolution: "@contember/react-block-repeater@workspace:packages/react-block-repeater" + dependencies: + "@contember/react-binding": "workspace:*" + "@contember/react-multipass-rendering": "workspace:*" + "@contember/react-repeater": "workspace:*" + "@contember/react-utils": "workspace:*" + "@radix-ui/react-slot": ^1.0.2 + react: ^18.2.0 + react-dom: ^18.2.0 + peerDependencies: + react: ^17 || ^18 + react-dom: ^17 || ^18 + languageName: unknown + linkType: soft + "@contember/react-board-dnd-kit@workspace:*, @contember/react-board-dnd-kit@workspace:packages/react-board-dnd-kit": version: 0.0.0-use.local resolution: "@contember/react-board-dnd-kit@workspace:packages/react-board-dnd-kit" @@ -3305,6 +3324,32 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-switch@npm:^1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-switch@npm:1.0.3" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-controllable-state": 1.0.1 + "@radix-ui/react-use-previous": 1.0.1 + "@radix-ui/react-use-size": 1.0.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: de18a802f317804d94315b1035d03a9cabef53317c148027f0f382bc2653723532691b65090596140737bb055e3affff977f5d73fe6caf8c526c6158baa811cc + languageName: node + linkType: hard + "@radix-ui/react-toast@npm:^1.1.5": version: 1.1.5 resolution: "@radix-ui/react-toast@npm:1.1.5"