diff --git a/build/api/react-ui-lib.api.md b/build/api/react-ui-lib.api.md index 0b9497a79..e00b797fc 100644 --- a/build/api/react-ui-lib.api.md +++ b/build/api/react-ui-lib.api.md @@ -223,10 +223,14 @@ export const baseEditorPlugins: { }; // @public (undocumented) -export type BaseFieldProps = Omit & UploaderBaseFieldProps; +export type BaseFieldProps = Omit & UploaderBaseFieldProps & { + dropzonePlaceholder?: ReactNode; +}; // @public (undocumented) -export type BaseFileRepeaterFieldProps = Omit & RepeaterProps & UploaderBaseFieldProps; +export type BaseFileRepeaterFieldProps = Omit & RepeaterProps & UploaderBaseFieldProps & { + dropzonePlaceholder?: ReactNode; +}; // @public (undocumented) export const Binding: ({ children }: { @@ -633,6 +637,8 @@ export type DataGridColumnProps = { name?: string; hidingName?: string; sortingField?: string; + cellClassName?: string; + headerClassName?: string; }; // @public (undocumented) @@ -908,6 +914,8 @@ export const DataGridTiles: React_2.NamedExoticComponent; export interface DataGridTilesProps { // (undocumented) children: React_2.ReactNode; + // (undocumented) + className?: string; } // @public (undocumented) @@ -994,7 +1002,8 @@ export const DefaultRepeater: NamedExoticComponent; // @public (undocumented) export type DefaultRepeaterProps = { - title?: string; + title?: ReactNode; + addButtonPosition?: 'none' | 'after' | 'before' | 'around'; } & RepeaterProps; // @public (undocumented) @@ -1793,8 +1802,9 @@ className?: string | undefined; }, "ref"> & RefAttributes>; // @public (undocumented) -export const RepeaterAddItemButton: ({ children }: { +export const RepeaterAddItemButton: ({ children, index }: { children?: React.ReactNode; + index?: RepeaterAddItemIndex; }) => JSX_2.Element; // @public (undocumented) @@ -2473,8 +2483,9 @@ export type UploadedVideoViewProps = FileUrlDataExtractorProps & GenericFileMeta }; // @public (undocumented) -export const UploaderDropzone: ({ inactiveOnUpload }: { +export const UploaderDropzone: ({ inactiveOnUpload, dropzonePlaceholder }: { inactiveOnUpload?: boolean; + dropzonePlaceholder?: ReactNode; }) => JSX_2.Element; // @public (undocumented) diff --git a/packages/playground/admin/app/components/UseEntity.tsx b/packages/playground/admin/app/components/UseEntity.tsx new file mode 100644 index 000000000..9c7babcf5 --- /dev/null +++ b/packages/playground/admin/app/components/UseEntity.tsx @@ -0,0 +1,12 @@ +import { Component, EntityAccessor, useEntity } from '@contember/interface' +import React from 'react' + +export const UseEntity = Component<{ render: (accessor?: EntityAccessor) => React.ReactNode }>(({ render }) => { + const entity = useEntity() + return <>{render(entity)} +}, +({ render }) => { + return <> + {render()} + +}) diff --git a/packages/playground/admin/app/pages/repeater.tsx b/packages/playground/admin/app/pages/repeater.tsx index 6d3af516a..86a7e306d 100644 --- a/packages/playground/admin/app/pages/repeater.tsx +++ b/packages/playground/admin/app/pages/repeater.tsx @@ -24,7 +24,7 @@ const repeaterDropdown = ( export default <> - + diff --git a/packages/playground/admin/app/pages/upload.tsx b/packages/playground/admin/app/pages/upload.tsx index f45fc528c..930e4e4a5 100644 --- a/packages/playground/admin/app/pages/upload.tsx +++ b/packages/playground/admin/app/pages/upload.tsx @@ -1,10 +1,91 @@ -import { EntitySubTree } from '@contember/react-binding' +import { EntitySubTree, StaticRender, useEntity } from '@contember/react-binding' import * as React from 'react' +import { useState } from 'react' import { Binding, PersistButton } from '@app/lib/binding' import { Slots } from '@app/lib/layout' import { AudioField, FileField, ImageField, ImageRepeaterField, VideoField } from '@app/lib/form' +import { Dialog, DialogContent, DialogTrigger } from '@app/lib/ui/dialog' +import { Button } from '@app/lib/ui/button' +import { DataGrid, DataGridColumn, DataGridLoader, DataGridPagination, DataGridTable, DataGridTextColumn, DataGridTiles, DataGridToolbar } from '@app/lib/datagrid' +import { UploadedImageView, UploaderDropzoneAreaUI } from '@app/lib/upload' +import { UseEntity } from '@app/app/components/UseEntity' +import { EntityAccessor, Field } from '@contember/interface' +import { FileUrlDataExtractorProps, GenericFileMetadataExtractorProps, ImageFileDataExtractorProps } from '@contember/react-uploader' +import { UploadIcon } from 'lucide-react' +import { dict } from '@app/lib/dict' +const imageFields: FileUrlDataExtractorProps & GenericFileMetadataExtractorProps & ImageFileDataExtractorProps = { + urlField: 'url', + widthField: 'width', + heightField: 'height', + fileNameField: 'meta.fileName', + fileSizeField: 'meta.fileSize', + fileTypeField: 'meta.fileType', + lastModifiedField: 'meta.lastModified', +} + +const SelectImage = () => { + const entity = useEntity() + + return { + entity.connectEntityAtField('image', it) + }} closeOnSelect /> +} + +const SelectImageRepeater = () => { + const entity = useEntity() + + return { + entity.getEntityList('imageList.items').createNewEntity(entity => { + entity().connectEntityAtField('image', it) + }) + }} /> +} + +const SelectImageInner = ({ connect, closeOnSelect }: { connect: (entity: EntityAccessor) => void, closeOnSelect?: boolean }) => { + const [open, setOpen] = useState(false) + return ( + + + + + + + + + + ( +
{ + it && connect(it) + closeOnSelect && setOpen(false) + }}> + +
+ )} /> +
+ + + ()} /> + + + + + + + + +
+ +
+
+
+ ) +} + export const image = () => <> @@ -12,20 +93,28 @@ export const image = () => <> + +
{dict.uploader.dropFiles}
+
{dict.uploader.or}
+
+ +
e.stopPropagation()}> + +
+
+ + )} />
+ export const imageTrivial = () => <> @@ -112,15 +201,22 @@ export const imageList = () => <> field="imageList.items" baseField="image" sortableBy="order" - urlField="url" - widthField="width" - heightField="height" - fileNameField="meta.fileName" - fileSizeField="meta.fileSize" - fileTypeField="meta.fileType" - lastModifiedField="meta.lastModified" + {...imageFields} label="Image file" description="Some description of the image file." + dropzonePlaceholder={( + + +
{dict.uploader.dropFiles}
+
{dict.uploader.or}
+
+ +
e.stopPropagation()}> + +
+
+
+ )} />
diff --git a/packages/react-ui-lib/src/datagrid/columns.tsx b/packages/react-ui-lib/src/datagrid/columns.tsx index 7734b61d6..e927d4d84 100644 --- a/packages/react-ui-lib/src/datagrid/columns.tsx +++ b/packages/react-ui-lib/src/datagrid/columns.tsx @@ -6,6 +6,7 @@ import { DataGridColumnHeader } from './column-header' import { DataViewElement } from '@contember/react-dataview' import { formatBoolean, formatDate, formatNumber } from '../formatting' import { DataGridEnumCell, DataGridHasManyCell, DataGridHasManyCellProps, DataGridHasOneCell, DataGridHasOneCellProps } from './cells' +import { cn } from '../utils' export const DataGridActionColumn = Component<{ children: ReactNode }>(({ children }) => ( (({ children, header, name, hidingName, sortingField }) => { +export const DataGridColumn = Component(({ children, header, name, hidingName, sortingField, cellClassName, headerClassName }) => { const wrapIsVisible = (child: ReactNode) => { const resolvedName = hidingName ?? name return resolvedName ? {child} : child @@ -140,14 +143,14 @@ export const DataGridColumn = Component(({ children, header name={name} header={ wrapIsVisible( - + {header ? {header} : null} , ) } - cell={wrapIsVisible({children})} + cell={wrapIsVisible({children})} /> ) }) diff --git a/packages/react-ui-lib/src/datagrid/tiles.tsx b/packages/react-ui-lib/src/datagrid/tiles.tsx index 015494184..c9c8a2e0d 100644 --- a/packages/react-ui-lib/src/datagrid/tiles.tsx +++ b/packages/react-ui-lib/src/datagrid/tiles.tsx @@ -3,18 +3,20 @@ import { DataViewEachRow, DataViewLayout } from '@contember/react-dataview' import * as React from 'react' import { dict } from '../dict' import { LayoutGridIcon } from 'lucide-react' +import { cn } from '../utils' export interface DataGridTilesProps { children: React.ReactNode + className?: string } -export const DataGridTiles = Component< DataGridTilesProps>(({ children }) => { +export const DataGridTiles = Component< DataGridTilesProps>(({ children, className }) => { return ( {dict.datagrid.showGrid} }> -
+
{children} diff --git a/packages/react-ui-lib/src/datagrid/toolbar.tsx b/packages/react-ui-lib/src/datagrid/toolbar.tsx index 66a030d76..a1e4190ab 100644 --- a/packages/react-ui-lib/src/datagrid/toolbar.tsx +++ b/packages/react-ui-lib/src/datagrid/toolbar.tsx @@ -53,7 +53,7 @@ export const DataGridToolbar = Component(({ children }) =>
-
+
{children ?? <> } diff --git a/packages/react-ui-lib/src/form/upload.tsx b/packages/react-ui-lib/src/form/upload.tsx index c4a1680d3..a53ce07a7 100644 --- a/packages/react-ui-lib/src/form/upload.tsx +++ b/packages/react-ui-lib/src/form/upload.tsx @@ -41,6 +41,9 @@ import { UploaderRepeaterDropIndicator } from '../upload/repeater' export type BaseFieldProps = & Omit & UploaderBaseFieldProps + & { + dropzonePlaceholder?: ReactNode + } @@ -52,7 +55,11 @@ export const ImageField = Component(props => { return ( - + {props.children ?? ( + + + + )} ) @@ -66,7 +73,11 @@ export const AudioField = Component(props => { return ( - + {props.children ?? ( + + + + )} ) @@ -81,7 +92,11 @@ export const VideoField = Component(props => { return ( - + {props.children ?? ( + + + + )} ) @@ -95,7 +110,11 @@ export const FileField = Component(props => { return ( - + {props.children ?? ( + + + + )} ) @@ -110,7 +129,7 @@ type UploadFieldInnerProps = children: ReactNode } -const UploadFieldInner = Component((({ baseField, label, description, children, fileType, urlField }: UploadFieldInnerProps) => { +const UploadFieldInner = Component((({ baseField, label, description, children, fileType, urlField, dropzonePlaceholder }: UploadFieldInnerProps) => { const entity = useEntity() const defaultUploader = useS3Client() const [fileTypeStable] = useState(fileType) @@ -136,16 +155,16 @@ const UploadFieldInner = Component((({ baseField, label, description, children, + { + entity = baseField ? entity.getEntity({ field: baseField }) : entity if (entity.getField(urlField).value === null) { - return + return } else { - return <>{children} + return children } }} /> - -
@@ -163,7 +182,9 @@ export type BaseFileRepeaterFieldProps = & Omit & RepeaterProps & UploaderBaseFieldProps - + & { + dropzonePlaceholder?: ReactNode + } export type ImageRepeaterFieldProps = & BaseFileRepeaterFieldProps @@ -171,7 +192,11 @@ export type ImageRepeaterFieldProps = export const ImageRepeaterField = Component(props => <> - + {props.children ?? ( + + + + )} ) @@ -182,7 +207,11 @@ export type AudioRepeaterFieldProps = export const AudioRepeaterField = Component(props => <> - + {props.children ?? ( + + + + )} ) @@ -193,7 +222,11 @@ export type VideoRepeaterFieldProps = export const VideoRepeaterField = Component(props => <> - + {props.children ?? ( + + + + )} ) @@ -204,7 +237,11 @@ export type FileRepeaterFieldProps = export const FileRepeaterField = Component(props => <> - + {props.children ?? ( + + + + )} ) @@ -216,7 +253,7 @@ type FileRepeaterFieldInnerProps = children: ReactNode } -const FileRepeaterFieldInner = Component(({ baseField, label, description, children, fileType, ...props }) => { +const FileRepeaterFieldInner = Component(({ baseField, label, description, children, fileType, dropzonePlaceholder, ...props }) => { const defaultUploader = useS3Client() const [fileTypeStable] = useState(fileType) @@ -243,7 +280,7 @@ const FileRepeaterFieldInner = Component(({ baseFie - + @@ -257,9 +294,7 @@ const FileRepeaterFieldInner = Component(({ baseFie - - {children} - + {children} @@ -270,9 +305,7 @@ const FileRepeaterFieldInner = Component(({ baseFie - - {children} - + {children} @@ -286,9 +319,8 @@ const FileRepeaterFieldInner = Component(({ baseFie }, ({ baseField, label, description, children, fileType, ...props }) => { return <> - - {children} - + + {children} }, 'FileRepeaterFieldInner') diff --git a/packages/react-ui-lib/src/layout/layout.tsx b/packages/react-ui-lib/src/layout/layout.tsx index f0dbe5fc7..ed2c424c8 100644 --- a/packages/react-ui-lib/src/layout/layout.tsx +++ b/packages/react-ui-lib/src/layout/layout.tsx @@ -7,6 +7,7 @@ import { Button } from '../ui/button' import { LogoutTrigger } from '@contember/react-identity' import { dict } from '../dict' import { useCurrentRequest } from '@contember/interface' +import { useStoredState } from '@contember/react-utils' const LayoutBodyUI = uic('div', { baseClass: 'bg-gray-50 h-full min-h-screen relative py-4 pl-[calc(100vw-100%)]' }) const LayoutMaxWidthUI = uic('div', { @@ -67,7 +68,7 @@ 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 [layout, setLayout] = useStoredState<'default' | 'stretch'>('local', ['', 'layout'], it => it ?? 'default') const request = useCurrentRequest() useEffect(() => { diff --git a/packages/react-ui-lib/src/repeater/repeater.tsx b/packages/react-ui-lib/src/repeater/repeater.tsx index 8ccc241b2..8e5748b34 100644 --- a/packages/react-ui-lib/src/repeater/repeater.tsx +++ b/packages/react-ui-lib/src/repeater/repeater.tsx @@ -1,6 +1,7 @@ import { Component } from '@contember/interface' import { Repeater, + RepeaterAddItemIndex, RepeaterAddItemTrigger, RepeaterEachItem, RepeaterEmpty, @@ -20,9 +21,10 @@ import { Button } from '../ui/button' import { uic } from '../utils/uic' import { DropIndicator } from '../ui/sortable' import { dict } from '../dict' +import { ReactNode } from 'react' export const RepeaterWrapperUI = 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', + baseClass: 'flex flex-col gap-2 p-4 pr-8 relative shadow-sm bg-white rounded border border-gray-300', }) export const RepeaterItemUI = uic('div', { baseClass: 'rounded border border-gray-300 p-4 relative', @@ -43,13 +45,13 @@ export const RepeaterDropIndicator = ({ position }: { position: 'before' | 'afte
) -export const RepeaterAddItemButton = ({ children }: { children?: React.ReactNode }) => ( - +export const RepeaterAddItemButton = ({ children, index }: { children?: React.ReactNode, index?: RepeaterAddItemIndex }) => ( +
@@ -70,10 +72,14 @@ export const RepeaterItemActions = uic('div', { baseClass: 'absolute top-1 right-2 flex gap-2', }) -export type DefaultRepeaterProps = { title?: string } +export type DefaultRepeaterProps = + & { + title?: ReactNode + addButtonPosition?: 'none' | 'after' | 'before' | 'around' + } & RepeaterProps -export const DefaultRepeater = Component(({ title, children, ...props }) => { +export const DefaultRepeater = Component(({ title, children, addButtonPosition = 'after', ...props }) => { const isSortable = props.sortableBy !== undefined if (!isSortable) { @@ -82,6 +88,7 @@ export const DefaultRepeater = Component(({ title, childre {title &&

{title}

} + {(addButtonPosition === 'before' || addButtonPosition === 'around') && }
@@ -95,7 +102,7 @@ export const DefaultRepeater = Component(({ title, childre - + {(addButtonPosition === 'after' || addButtonPosition === 'around') && }
) @@ -105,6 +112,7 @@ export const DefaultRepeater = Component(({ title, childre {title &&

{title}

} + {(addButtonPosition === 'before' || addButtonPosition === 'around') && }
@@ -132,7 +140,7 @@ export const DefaultRepeater = Component(({ title, childre - + {(addButtonPosition === 'after' || addButtonPosition === 'around') && }
diff --git a/packages/react-ui-lib/src/ui/input.tsx b/packages/react-ui-lib/src/ui/input.tsx index 259bca0bc..d7d813220 100644 --- a/packages/react-ui-lib/src/ui/input.tsx +++ b/packages/react-ui-lib/src/ui/input.tsx @@ -52,6 +52,9 @@ export const InputBare = uic('input', { export const CheckboxInput = uic('input', { baseClass: 'w-4 h-4', + defaultProps: { + type: 'checkbox', + }, }) export const RadioInput = uic('input', { diff --git a/packages/react-ui-lib/src/upload/dropzone.tsx b/packages/react-ui-lib/src/upload/dropzone.tsx index 1d833f2cf..7431b1137 100644 --- a/packages/react-ui-lib/src/upload/dropzone.tsx +++ b/packages/react-ui-lib/src/upload/dropzone.tsx @@ -2,9 +2,13 @@ import * as React from 'react' import { UploaderDropzoneArea, UploaderDropzoneRoot } from '@contember/react-uploader-dropzone' import { useUploaderStateFiles } from '@contember/react-uploader' import { UploaderDropzoneAreaUI, UploaderDropzoneWrapperUI, UploaderInactiveDropzoneUI } from './ui' +import { ReactNode } from 'react' +import { UploadIcon } from 'lucide-react' +import { dict } from '../dict' +import { Button } from '../ui/button' -export const UploaderDropzone = ({ inactiveOnUpload }: { inactiveOnUpload?: boolean }) => { +export const UploaderDropzone = ({ inactiveOnUpload, dropzonePlaceholder }: { inactiveOnUpload?: boolean, dropzonePlaceholder?: ReactNode }) => { const filesInProgress = useUploaderStateFiles({ state: ['uploading', 'initial', 'finalizing'] }) const showLoader = inactiveOnUpload && filesInProgress.length > 0 @@ -13,7 +17,14 @@ export const UploaderDropzone = ({ inactiveOnUpload }: { inactiveOnUpload?: bool {showLoader ? - : + : {dropzonePlaceholder ?? + +
{dict.uploader.dropFiles}
+
{dict.uploader.or}
+
+ +
+
}
}
diff --git a/packages/react-ui-lib/src/upload/ui.tsx b/packages/react-ui-lib/src/upload/ui.tsx index dfcd551f7..f61a7226f 100644 --- a/packages/react-ui-lib/src/upload/ui.tsx +++ b/packages/react-ui-lib/src/upload/ui.tsx @@ -1,8 +1,6 @@ import * as React from 'react' import { Loader } from '../ui/loader' -import { GripIcon, GripVerticalIcon, UploadIcon } from 'lucide-react' -import { dict } from '../dict' -import { Button } from '../ui/button' +import { GripIcon } from 'lucide-react' import { uic } from '../utils/uic' export const UploaderDropzoneWrapperUI = uic('div', { baseClass: 'rounded border p-1 shadow' }) @@ -21,14 +19,6 @@ export const UploaderDropzoneAreaUI = uic('div', { defaultVariants: { size: 'square', }, - beforeChildren: <> - -
{dict.uploader.dropFiles}
-
{dict.uploader.or}
-
- -
- , }) diff --git a/packages/react-ui-lib/src/upload/view.tsx b/packages/react-ui-lib/src/upload/view.tsx index 6ca3e3fff..24bedbb85 100644 --- a/packages/react-ui-lib/src/upload/view.tsx +++ b/packages/react-ui-lib/src/upload/view.tsx @@ -2,11 +2,11 @@ import * as React from 'react' import { ComponentType, ReactNode } from 'react' import { Component, Field, SugaredRelativeSingleField, useEntity, useField } from '@contember/interface' import { AudioFileDataExtractorProps, FileUrlDataExtractorProps, GenericFileMetadataExtractorProps, ImageFileDataExtractorProps, VideoFileDataExtractorProps } from '@contember/react-uploader' -import { formatBytes, formatDate, formatDuration } from '../formatting/formatting' +import { formatBytes, formatDate, formatDuration } from '../formatting' import { Button } from '../ui/button' import { FileIcon, InfoIcon, TrashIcon } from 'lucide-react' import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover' -import { formatImageResizeUrl } from '../images/formatImageResizeUrl' +import { formatImageResizeUrl } from '../images' export type UploadedImageViewProps = & FileUrlDataExtractorProps @@ -26,18 +26,20 @@ export const UploadedImageView = Component(({ DestroyAct
) -}, () => { - // todo - return null +}, props => { + return <> + + + }) -const ImageMetadata = ({ heightField, widthField, ...props }: UploadedImageViewProps) => { +const ImageMetadata = Component(({ heightField, widthField, ...props }: UploadedImageViewProps) => { return ( ) -} +}) export type UploadedAudioViewProps = @@ -58,9 +60,11 @@ export const UploadedAudioView = Component(({ DestroyAct ) -}, () => { - // todo - return null +}, props => { + return <> + + + }) const AudioMetadata = ({ durationField, ...props }: UploadedAudioViewProps) => { @@ -90,19 +94,21 @@ export const UploadedVideoView = Component(({ DestroyAct ) -}, () => { - // todo - return null +}, props => { + return <> + + + }) -const VideoMetadata = ({ durationField, widthField, heightField, ...props }: UploadedVideoViewProps) => { +const VideoMetadata = Component(({ durationField, widthField, heightField, ...props }: UploadedVideoViewProps) => { return ( ) -} +}) export type UploadedAnyViewProps = & FileUrlDataExtractorProps @@ -137,7 +143,7 @@ type MetadataProps = children?: ReactNode } -const Metadata = ({ children, urlField, fileSizeField, fileNameField, lastModifiedField, fileTypeField }: MetadataProps) => { +const Metadata = Component(({ children, urlField, fileSizeField, fileNameField, lastModifiedField, fileTypeField }: MetadataProps) => { return (
@@ -152,9 +158,9 @@ const Metadata = ({ children, urlField, fileSizeField, fileNameField, lastModifi )} />
) -} +}) -const DimensionsMeta = ({ widthField, heightField }: { +const DimensionsMeta = Component(({ widthField, heightField }: { widthField?: SugaredRelativeSingleField['field'] heightField?: SugaredRelativeSingleField['field'] }) => { @@ -171,14 +177,19 @@ const DimensionsMeta = ({ widthField, heightField }: { {width} x {height} px ) +}, ({ widthField, heightField }) => { + return <> + {widthField && } + {heightField && } + +}) -} - -const MetaField = ({ field, label, format = it => it }: { +interface MetaFieldProps { field?: SugaredRelativeSingleField['field'] label: ReactNode format?: (value: any) => ReactNode -}) => { +} +const MetaField = Component(({ field, label, format = it => it }) => { const entity = useEntity() return field ? ( <> @@ -188,7 +199,9 @@ const MetaField = ({ field, label, format = it => it }: { ) : null -} +}, ({ field }) => { + return field ? : null +}) const FileActions = ({ DestroyAction, children }: {