diff --git a/AGENTS.md b/AGENTS.md index 319620911cd..571b8f16676 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ When new backend features are discovered, update `ai-context/core-features-refer ## Building - When type checking, use `yarn check -p `, e.g., `yarn check -p @webiny/api-core` -- When building a single package, use `yarn build -p `, e.g., `yarn build -p @webiny/api-core`. +- When building a single package, use `yarn build -p --safe-replace`, e.g., `yarn build -p @webiny/api-core --safe-replace`. We use "--safe-replace" in order to not have our active bundling watch process break. - To build all packages, simply run `yarn build`. - To build all packages without caching, use `yarn build --no-cache `. diff --git a/packages/admin-ui/src/theme.css b/packages/admin-ui/src/theme.css index 8e130a9e564..394795083b1 100644 --- a/packages/admin-ui/src/theme.css +++ b/packages/admin-ui/src/theme.css @@ -351,7 +351,7 @@ @utility fill-grid { background-image: radial-gradient(var(--color-neutral-strong) 1px, transparent 0px); background-size: 15px 15px; - @apply bg-neutral-subtle; + @apply bg-neutral-light; } @layer base { diff --git a/packages/app-headless-cms/src/admin/components/ContentModelEditor/ContentModelEditor.tsx b/packages/app-headless-cms/src/admin/components/ContentModelEditor/ContentModelEditor.tsx index c38b54dd0e2..6a965385c83 100644 --- a/packages/app-headless-cms/src/admin/components/ContentModelEditor/ContentModelEditor.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentModelEditor/ContentModelEditor.tsx @@ -1,11 +1,17 @@ import React, { useState, useEffect } from "react"; -import { makeDecoratable, useDialogs, LeftPanel, RightPanel, SplitView } from "@webiny/app-admin"; +import { makeDecoratable, useDialogs } from "@webiny/app-admin"; import { useRouter } from "@webiny/app"; import { i18n } from "@webiny/app/i18n/index.js"; -import { Heading, OverlayLoader, Tabs, Text, TimeAgo } from "@webiny/admin-ui"; -import { ReactComponent as EditIcon } from "@webiny/icons/edit.svg"; -import { ReactComponent as PreviewIcon } from "@webiny/icons/fullscreen.svg"; +import { IconButton, OverlayLoader, SegmentedControlPrimitive, Tag, Text } from "@webiny/admin-ui"; +import { ReactComponent as ExpandSidebarIcon } from "@webiny/icons/left_panel_open.svg"; +import { ReactComponent as ViewCompactAltIcon } from "@webiny/icons/view_compact_alt.svg"; +import { ReactComponent as ViewDayIcon } from "@webiny/icons/view_day.svg"; import { FieldsSidebar } from "./FieldsSidebar.js"; + +const VIEW_ITEMS = [ + { id: "compact", value: "compact", label: "", icon: }, + { id: "full", value: "full", label: "", icon: } +]; import { FieldEditor } from "../FieldEditor/index.js"; import { PreviewTab } from "./PreviewTab.js"; import Header from "./Header.js"; @@ -27,7 +33,7 @@ interface OnChangeParams { } export const ContentModelEditor = makeDecoratable("ContentModelEditor", () => { - const { data, setData, isPristine, contentModel } = useModelEditor(); + const { data, setData, isPristine } = useModelEditor(); const router = useRouter(); const dialogs = useDialogs(); @@ -60,6 +66,8 @@ export const ContentModelEditor = makeDecoratable("ContentModelEditor", () => { }, []); const [activeTab, setActiveTab] = useState("edit"); + const [isSidebarOpen, setIsSidebarOpen] = useState(true); + const [activeView, setActiveView] = useState("compact"); const onChange = ({ fields, layout }: OnChangeParams) => { setData(data => ({ ...data, fields, layout })); @@ -73,75 +81,121 @@ export const ContentModelEditor = makeDecoratable("ContentModelEditor", () => { return (
-
+
- - -
+
+
+ {/* Expanded content */} +
{ - setActiveTab("edit"); - }} + onFieldDragStart={() => setActiveTab("edit")} + onCollapse={() => setIsSidebarOpen(false)} + /> +
+ {/* Collapsed strip */} +
+ } + onClick={() => setIsSidebarOpen(true)} /> + + Fields +
- - -
- {contentModel && ( -
- {contentModel.name} - - {`Created by ${contentModel.createdBy.displayName}. Last modified: `} - . +
+
+
+
+ {activeTab === "edit" && ( + + Model editor -
- )} - } - data-testid={"cms.editor.tab.edit"} - content={ -
- -
- } - />, - } - data-testid={"cms.editor.tab.preview"} - content={ - - - - - - - - } - /> - ]} + )} + {activeTab === "preview" && ( + + )} +
+
- - +
+
+ {activeTab === "edit" && ( +
+ +
+ )} + {activeTab === "preview" && ( +
+ + + + + + + +
+ )} +
+
+
+
diff --git a/packages/app-headless-cms/src/admin/components/ContentModelEditor/FieldsGrid.tsx b/packages/app-headless-cms/src/admin/components/ContentModelEditor/FieldsGrid.tsx new file mode 100644 index 00000000000..b8dd57060fd --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentModelEditor/FieldsGrid.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +interface FieldsGridProps { + children: React.ReactNode; +} + +const FieldsGrid = ({ children }: FieldsGridProps) => ( +
{children}
+); + +export { FieldsGrid, type FieldsGridProps }; diff --git a/packages/app-headless-cms/src/admin/components/ContentModelEditor/FieldsSidebar.tsx b/packages/app-headless-cms/src/admin/components/ContentModelEditor/FieldsSidebar.tsx index 8d6fd6d83bc..a613742a2fc 100644 --- a/packages/app-headless-cms/src/admin/components/ContentModelEditor/FieldsSidebar.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentModelEditor/FieldsSidebar.tsx @@ -3,48 +3,28 @@ import React from "react"; import { plugins } from "@webiny/plugins"; import Draggable from "../Draggable.js"; import type { CmsModelFieldTypePlugin, CmsModelLayoutFieldTypePlugin } from "~/types.js"; -import { Heading, Icon, Text } from "@webiny/admin-ui"; +import { IconButton } from "@webiny/admin-ui"; +import { GridItem } from "./GridItem.js"; +import { SectionHeader } from "./SectionHeader.js"; +import { FieldsGrid } from "./FieldsGrid.js"; +import { ReactComponent as CollapseSidebarIcon } from "@webiny/icons/right_panel_open.svg"; interface FieldProps { onFieldDragStart: DragEventHandler; fieldType: CmsModelFieldTypePlugin["field"]; } -const Field = (props: FieldProps) => { - const { - onFieldDragStart, - fieldType: { type, label, icon, description } - } = props; +const Field = ({ onFieldDragStart, fieldType: { type, label, icon } }: FieldProps) => { return ( {({ drag }) => ( -
{ - drag(element); - }} - data-testid={`cms-editor-fields-field-${type}`} + -
-
- -
-
- {label} - - {description} - -
-
-
+ dragRef={element => drag(element)} + /> )}
); @@ -55,36 +35,20 @@ interface LayoutFieldItemProps { layoutField: CmsModelLayoutFieldTypePlugin["field"]; } -const LayoutFieldItem = (props: LayoutFieldItemProps) => { - const { - onFieldDragStart, - layoutField: { type, label, icon, description } - } = props; +const LayoutFieldItem = ({ + onFieldDragStart, + layoutField: { type, label, icon } +}: LayoutFieldItemProps) => { return ( {({ drag }) => ( -
{ - drag(element); - }} - data-testid={`cms-editor-fields-layout-field-${type}`} + -
-
- -
-
- {label} - - {description} - -
-
-
+ dragRef={element => drag(element)} + /> )}
); @@ -92,10 +56,11 @@ const LayoutFieldItem = (props: LayoutFieldItemProps) => { interface FieldsSidebarProps { onFieldDragStart: DragEventHandler; + onCollapse?: () => void; } -export const FieldsSidebar = ({ onFieldDragStart }: FieldsSidebarProps) => { - const fieldTypePlugin = plugins +export const FieldsSidebar = ({ onFieldDragStart, onCollapse }: FieldsSidebarProps) => { + const fieldTypePlugins = plugins .byType("cms-editor-field-type") .filter(p => !p.field.hideInAdmin); @@ -105,36 +70,40 @@ export const FieldsSidebar = ({ onFieldDragStart }: FieldsSidebarProps) => { return ( <> - - Fields - - {fieldTypePlugin.map(fieldPlugin => ( - - ))} + } + onClick={onCollapse} + /> + ) + } + /> + + {fieldTypePlugins.map(fieldPlugin => ( + + ))} + {layoutFieldPlugins.length > 0 && ( <> - - Layout - - {layoutFieldPlugins.map(lp => ( - - ))} + + + {layoutFieldPlugins.map(lp => ( + + ))} + )} diff --git a/packages/app-headless-cms/src/admin/components/ContentModelEditor/GridItem.tsx b/packages/app-headless-cms/src/admin/components/ContentModelEditor/GridItem.tsx new file mode 100644 index 00000000000..e3339f85e4f --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentModelEditor/GridItem.tsx @@ -0,0 +1,35 @@ +import type { DragEventHandler } from "react"; +import React from "react"; +import { Icon } from "@webiny/admin-ui"; + +interface GridItemProps { + testId: string; + label: string; + icon: React.ReactElement; + onDragStart: DragEventHandler; + dragRef: (element: HTMLElement | null) => void; +} + +const GridItem = ({ testId, label, icon, onDragStart, dragRef }: GridItemProps) => { + return ( +
+ + + {label} + +
+ ); +}; + +export { GridItem, type GridItemProps }; diff --git a/packages/app-headless-cms/src/admin/components/ContentModelEditor/Header.tsx b/packages/app-headless-cms/src/admin/components/ContentModelEditor/Header.tsx index 06e8021a860..d5efbec3fd0 100644 --- a/packages/app-headless-cms/src/admin/components/ContentModelEditor/Header.tsx +++ b/packages/app-headless-cms/src/admin/components/ContentModelEditor/Header.tsx @@ -1,8 +1,20 @@ import React from "react"; -import { HeaderBar } from "@webiny/admin-ui"; +import { HeaderBar, SegmentedControlPrimitive } from "@webiny/admin-ui"; import { renderPlugins } from "@webiny/app/plugins/index.js"; +import { ReactComponent as EditIcon } from "@webiny/icons/dashboard.svg"; +import { ReactComponent as PreviewIcon } from "@webiny/icons/table_chart.svg"; -const EditorBar = () => { +const EDITOR_TABS = [ + { id: "edit", label: "Edit", value: "edit", icon: }, + { id: "preview", label: "Preview", value: "preview", icon: } +]; + +interface EditorBarProps { + activeTab: string; + onTabChange: (tab: string) => void; +} + +const EditorBar = ({ activeTab, onTabChange }: EditorBarProps) => { return ( { {renderPlugins("content-model-editor-default-bar-left")}
} + middle={ + + } end={
{renderPlugins("content-model-editor-default-bar-right")} diff --git a/packages/app-headless-cms/src/admin/components/ContentModelEditor/SectionHeader.tsx b/packages/app-headless-cms/src/admin/components/ContentModelEditor/SectionHeader.tsx new file mode 100644 index 00000000000..df60f3bbda0 --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/ContentModelEditor/SectionHeader.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { Text } from "@webiny/admin-ui"; + +interface SectionHeaderProps { + title: string; + action?: React.ReactNode; +} + +const SectionHeader = ({ title, action }: SectionHeaderProps) => ( +
+ + {title} + + {action} +
+); + +export { SectionHeader, type SectionHeaderProps }; diff --git a/packages/app-headless-cms/src/admin/components/DragPreview.tsx b/packages/app-headless-cms/src/admin/components/DragPreview.tsx index ea16a965114..abaf4abd188 100644 --- a/packages/app-headless-cms/src/admin/components/DragPreview.tsx +++ b/packages/app-headless-cms/src/admin/components/DragPreview.tsx @@ -1,89 +1,70 @@ -import React, { useEffect, useState } from "react"; -import type { DragSourceMonitor } from "react-dnd"; +import React, { useEffect, useRef, useState } from "react"; +import type { DragLayerMonitor } from "react-dnd"; import { useDragLayer } from "react-dnd"; -import type { DragSourceMonitorImpl } from "~/types.js"; - -let subscribedToOffsetChange = false; +import { DragCursor } from "@webiny/admin-ui"; +import { dropZoneOverState } from "./dropZoneOverState.js"; +import { getDragInfo } from "./getDragInfo.js"; +import type { DragSource } from "~/types.js"; let dragPreviewRef: HTMLDivElement | null = null; -const onOffsetChange = (monitor: DragSourceMonitor) => () => { - if (!dragPreviewRef) { - return; - } - - const offset = monitor.getClientOffset(); - if (!offset) { - return; - } +const DragPreview = () => { + const [opacity, setOpacity] = useState(0); + const [isOverSlot, setIsOverSlot] = useState(false); + const monitorRef = useRef(null); - const transform = `translate(${offset.x - 15}px, ${offset.y - 15}px)`; - dragPreviewRef.style["transform"] = transform; - // TODO @ts-refactor figure out better type / possibly not needed in newer browsers - // @ts-expect-error - dragPreviewRef.style["-webkit-transform"] = transform; -}; + const { isDragging, item } = useDragLayer((monitor: DragLayerMonitor) => { + monitorRef.current = monitor; -const DragPreview = () => { - const [dragHelperOpacity, setDragHelperOpacity] = useState(0); - const { isDragging } = useDragLayer(initialMonitor => { - /** - * We must cast because TS is complaining. We know that casting as DragSourceMonitorImpl is ok. - */ - const monitor = initialMonitor as unknown as DragSourceMonitorImpl; - if (!subscribedToOffsetChange) { - monitor.subscribeToOffsetChange(onOffsetChange(monitor)); - subscribedToOffsetChange = true; + const offset = monitor.getClientOffset(); + if (offset && dragPreviewRef) { + dragPreviewRef.style.transform = `translate(${offset.x + 12}px, ${offset.y + 12}px)`; } return { - isDragging: monitor.isDragging() + isDragging: monitor.isDragging(), + item: monitor.getItem() as DragSource | null }; }); + useEffect(() => { + if (isDragging) { + const t = setTimeout(() => setOpacity(1), 80); + return () => clearTimeout(t); + } + setOpacity(0); + return undefined; + }, [isDragging]); + + useEffect(() => { + return dropZoneOverState.subscribe(setIsOverSlot); + }, []); + useEffect(() => { return () => { - subscribedToOffsetChange = false; dragPreviewRef = null; }; }, []); - // We track the value of "isDragging" and apply opacity=1 (after 100ms), when it switches to true. - // Without this, the drag cursor would be shown in the top-left corner for a short amount of time, and then it - // would be repositioned correctly. Definitely looks like a glitch. This also adds a nice little fade-in effect. - useEffect(() => { - if (isDragging) { - setTimeout(() => { - setDragHelperOpacity(isDragging ? 1 : 0); - }, 100); - } else { - setDragHelperOpacity(0); - } - }, [isDragging]); - if (!isDragging) { return null; } - if (!isDragging) { - return null; - } + const { label, icon } = getDragInfo(item); return ( -
+
{ dragPreviewRef = el; }} - className="transition-opacity duration-250 ease-in-out block" - style={{ opacity: dragHelperOpacity }} + className={"absolute transition-opacity duration-100"} + style={{ opacity }} > -
+
); }; + export default DragPreview; diff --git a/packages/app-headless-cms/src/admin/components/DropZone/Horizontal.tsx b/packages/app-headless-cms/src/admin/components/DropZone/Horizontal.tsx index 2ad7a81ee03..c914222ca23 100644 --- a/packages/app-headless-cms/src/admin/components/DropZone/Horizontal.tsx +++ b/packages/app-headless-cms/src/admin/components/DropZone/Horizontal.tsx @@ -2,37 +2,8 @@ import React from "react"; import type { IsVisibleCallable } from "../Droppable.js"; import { Droppable } from "../Droppable.js"; import type { DragSource } from "~/types.js"; -import { cn } from "@webiny/admin-ui"; - -interface OuterDivProps { - isOver: boolean; - isDragging: boolean; - last: boolean; -} - -const OuterDiv = ({ isOver, isDragging, last }: OuterDivProps) => ( -
-
-
-
-
-); +import { Icon } from "@webiny/admin-ui"; +import { ReactComponent as AddIcon } from "@webiny/icons/add.svg"; interface HorizontalProps { onDrop(item: DragSource): void; @@ -50,17 +21,24 @@ const Horizontal = ({ last, onDrop, isVisible, ...rest }: HorizontalProps) => { drop(element); }} data-testid={rest["data-testid"]} - style={{ - /* For dropzone debugging: border: "1px solid blue",*/ - height: "16px", - width: "100%", - position: "absolute", - [last ? "bottom" : "top"]: 0, - left: 0, - zIndex: isDragging ? 1000 : -1 - }} + className={`h-sm-extra w-full absolute left-0 ${last ? "-bottom-sm-extra" : "-top-sm-extra"} ${isDragging ? "z-[1000]" : "-z-[1]"}`} > - + {isDragging && ( +
+
+ } label="Add" size="xs" color="accent" /> +
+
+ )}
)} diff --git a/packages/app-headless-cms/src/admin/components/DropZone/Vertical.tsx b/packages/app-headless-cms/src/admin/components/DropZone/Vertical.tsx index aaad7d0bf5c..4880601982c 100644 --- a/packages/app-headless-cms/src/admin/components/DropZone/Vertical.tsx +++ b/packages/app-headless-cms/src/admin/components/DropZone/Vertical.tsx @@ -2,38 +2,8 @@ import React from "react"; import type { IsVisibleCallable } from "../Droppable.js"; import { Droppable } from "../Droppable.js"; import type { DragSource } from "~/types.js"; -import { cn } from "@webiny/admin-ui"; - -interface OuterDivVerticalProps { - isOver: boolean; - last?: boolean; - isDragging?: boolean; -} - -const OuterDivVertical = ({ isOver, last, isDragging }: OuterDivVerticalProps) => ( -
-
-
-
-
-); +import { Icon } from "@webiny/admin-ui"; +import { ReactComponent as AddIcon } from "@webiny/icons/add.svg"; interface VerticalProps { depth?: number; @@ -50,18 +20,31 @@ const Vertical = ({ depth, last, onDrop, isVisible }: VerticalProps) => { ref={element => { drop(element); }} - style={{ - /* For dropzone debugging: border: "1px solid blue",*/ - width: "30%", - maxWidth: "100px", - height: "100%", - position: "absolute", - top: 0, - [last ? "right" : "left"]: 0, - zIndex: isDragging ? 1000 + (depth || 0) : -1 - }} + className={`w-sm-extra h-full absolute top-0 ${last ? "right-0" : "left-0"}`} + style={{ zIndex: isDragging ? 1000 + (depth || 0) : -1 }} > - + {isDragging && ( +
+
+ } + label="Add" + size="xs" + className={"fill-warning-600"} + /> +
+
+ )}
)} diff --git a/packages/app-headless-cms/src/admin/components/Droppable.tsx b/packages/app-headless-cms/src/admin/components/Droppable.tsx index 310da0a4cb6..1e9f857c055 100644 --- a/packages/app-headless-cms/src/admin/components/Droppable.tsx +++ b/packages/app-headless-cms/src/admin/components/Droppable.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import type { ConnectDropTarget } from "react-dnd"; import { useDrop } from "react-dnd"; import type { DragSource } from "~/types.js"; +import { dropZoneOverState } from "./dropZoneOverState.js"; export interface DroppableChildrenFunctionParams { isDragging: boolean; @@ -59,6 +60,21 @@ const DroppableComponent = (props: DroppableProps) => { } }); + const prevIsOverRef = React.useRef(false); + React.useEffect(() => { + if (isOver === prevIsOverRef.current) { + return; + } + dropZoneOverState.notify(isOver); + prevIsOverRef.current = isOver; + return () => { + if (prevIsOverRef.current) { + dropZoneOverState.notify(false); + prevIsOverRef.current = false; + } + }; + }, [isOver]); + if (item && !isVisible(item)) { return null; } diff --git a/packages/app-headless-cms/src/admin/components/FieldEditor/Field.tsx b/packages/app-headless-cms/src/admin/components/FieldEditor/Field.tsx index 806f3e72c27..947993982f3 100644 --- a/packages/app-headless-cms/src/admin/components/FieldEditor/Field.tsx +++ b/packages/app-headless-cms/src/admin/components/FieldEditor/Field.tsx @@ -10,7 +10,7 @@ import { i18n } from "@webiny/app/i18n/index.js"; import { useModelEditor } from "~/admin/hooks/index.js"; import { useModelFieldEditor } from "~/admin/components/FieldEditor/useModelFieldEditor.js"; import { useSnackbar } from "@webiny/app-admin"; -import { IconButton, Heading, Text, DropdownMenu, Tag } from "@webiny/admin-ui"; +import { Icon, IconButton, Text, DropdownMenu, Tag } from "@webiny/admin-ui"; const t = i18n.ns("app-headless-cms/admin/components/editor/field"); @@ -168,7 +168,7 @@ const Field = (props: FieldProps) => { if (!fieldTypeName) { return null; } - return ; + return ; }; fn.displayName = "FieldTypeRenderer"; @@ -184,32 +184,49 @@ const Field = (props: FieldProps) => { return ( -
-
- {field.label} - - {fieldPlugin.field.label} - {info && ({info})} - +
+
+ +
+ + {field.label} + + + {fieldPlugin.field.label} + {info && / {info}} + +
-
+
{fieldInformationRenderer ? fieldInformationRenderer({ model, field }) : defaultInformationRenderer()} - {canEdit ? ( - } - onClick={() => onEdit(field)} - variant={"ghost"} - size={"sm"} - /> - ) : null} } variant={"ghost"} size={"sm"} /> } > + {canEdit && ( + onEdit(field)} + text={t`Edit`} + icon={ + } + label={t`Edit field`} + /> + } + /> + )} {editorFieldOptionPlugins.map(pl => React.cloneElement(pl.render(), { key: pl.name }) )} diff --git a/packages/app-headless-cms/src/admin/components/FieldEditor/FieldEditor.tsx b/packages/app-headless-cms/src/admin/components/FieldEditor/FieldEditor.tsx index 05084e61c68..b599154e918 100644 --- a/packages/app-headless-cms/src/admin/components/FieldEditor/FieldEditor.tsx +++ b/packages/app-headless-cms/src/admin/components/FieldEditor/FieldEditor.tsx @@ -14,7 +14,7 @@ import type { CmsEditorFieldsLayout, CmsModelField, DragSource } from "~/types.j import type { CmsLayoutField } from "@webiny/app-headless-cms-common/types/model.js"; import { isLayoutField } from "@webiny/app-headless-cms-common/types/model.js"; import { ModelFieldProvider } from "~/admin/components/ModelFieldProvider/index.js"; -import { cn, Icon } from "@webiny/admin-ui"; +import { IconButton } from "@webiny/admin-ui"; const t = i18n.namespace("app-headless-cms/admin/components/editor"); @@ -148,28 +148,20 @@ const Editor = () => { } /* RowContainer start - includes drag handle, drop zones and the Row itself. */ ) => (
{ drag(element); }} > - } - label={"Drag to move this row"} - color={"neutral-light"} - size={"sm"} + variant={"secondary"} + size={"xs"} />
{ /> {/* Row start - includes field drop zones and fields */}
{row.map((cell, fieldIndex) => { @@ -203,15 +192,11 @@ const Editor = () => { ref={element => { drag(element); }} - className={cn([ - "relative", - "flex-1 basis-full", - "mx-sm" - ])} + className={"relative flex-1 min-w-0"} >
{ > {({ drag }) => (
{ drag(element); }} @@ -277,7 +258,7 @@ const Editor = () => {
void; + +class DropZoneOverState { + private subscribers = new Set(); + private activeCount = 0; + + notify(isOver: boolean) { + this.activeCount = isOver ? this.activeCount + 1 : Math.max(0, this.activeCount - 1); + const state = this.activeCount > 0; + this.subscribers.forEach(fn => fn(state)); + } + + subscribe(fn: Subscriber) { + this.subscribers.add(fn); + return () => { + this.subscribers.delete(fn); + }; + } +} + +export const dropZoneOverState = new DropZoneOverState(); diff --git a/packages/app-headless-cms/src/admin/components/getDragInfo.tsx b/packages/app-headless-cms/src/admin/components/getDragInfo.tsx new file mode 100644 index 00000000000..91d2d19ec19 --- /dev/null +++ b/packages/app-headless-cms/src/admin/components/getDragInfo.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { plugins } from "@webiny/plugins"; +import type { + CmsModelFieldTypePlugin, + CmsModelLayoutFieldTypePlugin, + DragSource +} from "~/types.js"; + +export interface DragInfo { + label: string; + icon?: React.ReactElement; +} + +export const getDragInfo = (item: DragSource | null): DragInfo => { + if (!item) { + return { label: "" }; + } + + if (item.type === "newField" && item.fieldType) { + const plugin = plugins + .byType("cms-editor-field-type") + .find(p => p.field.type === item.fieldType); + return { + label: plugin?.field.label ?? item.fieldType, + icon: plugin?.field.icon as React.ReactElement | undefined + }; + } + + if (item.type === "field" && item.field) { + const plugin = plugins + .byType("cms-editor-field-type") + .find(p => p.field.type === item.field!.type); + return { + label: item.field.label, + icon: plugin?.field.icon as React.ReactElement | undefined + }; + } + + if (item.type === "newLayoutField" && item.layoutFieldType) { + const plugin = plugins + .byType("cms-editor-layout-field-type") + .find(p => p.field.type === item.layoutFieldType); + return { + label: plugin?.field.label ?? item.layoutFieldType, + icon: plugin?.field.icon as React.ReactElement | undefined + }; + } + + if (item.type === "layoutField" && item.layoutField) { + const plugin = plugins + .byType("cms-editor-layout-field-type") + .find(p => p.field.type === item.layoutField!.type); + return { + label: plugin?.field.label ?? "Layout", + icon: plugin?.field.icon as React.ReactElement | undefined + }; + } + + if (item.type === "row") { + return { label: "Row" }; + } + + return { label: "Field" }; +}; diff --git a/packages/app-headless-cms/src/admin/plugins/editor/defaultBar/FormSettings/FormSettings.tsx b/packages/app-headless-cms/src/admin/plugins/editor/defaultBar/FormSettings/FormSettings.tsx index a98cbe83305..98f87986c63 100644 --- a/packages/app-headless-cms/src/admin/plugins/editor/defaultBar/FormSettings/FormSettings.tsx +++ b/packages/app-headless-cms/src/admin/plugins/editor/defaultBar/FormSettings/FormSettings.tsx @@ -1,105 +1,67 @@ -import React, { useState } from "react"; +import React, { useRef, useState } from "react"; import { plugins } from "@webiny/plugins"; -import { - LeftPanel, - OverlayLayout, - RightPanel, - SimpleForm, - SimpleFormContent, - SimpleFormFooter, - SimpleFormHeader, - SplitView, - useSnackbar -} from "@webiny/app-admin"; +import { useSnackbar } from "@webiny/app-admin"; import { Form } from "@webiny/form"; import { i18n } from "@webiny/app/i18n/index.js"; import type { CmsEditorFormSettingsPlugin } from "~/types.js"; import { useModelEditor } from "~/admin/hooks/index.js"; -import { Button, Heading, Icon, List } from "@webiny/admin-ui"; +import { Drawer, Icon, List } from "@webiny/admin-ui"; const t = i18n.namespace("FormsApp.Editor.FormSettings"); -const Title = () => { - return ( - {t`Content model settings`} - ); -}; - interface FormSettingsProps { - onExited: () => void; + open: boolean; + onClose: () => void; } -const FormSettings = ({ onExited }: FormSettingsProps) => { +const FormSettings = ({ open, onClose }: FormSettingsProps) => { const cmsEditorFormSettingsPlugins = plugins.byType( "cms-editor-form-settings" ); const { data, setData } = useModelEditor(); const { showSnackbar } = useSnackbar(); - const [activePlugin, setActivePlugin] = useState(cmsEditorFormSettingsPlugins[0]); + const submitRef = useRef<((e?: React.SyntheticEvent) => void) | null>(null); return ( - } onExited={onExited}> - - - - {cmsEditorFormSettingsPlugins.map(pl => ( - setActivePlugin(pl)} - icon={} - title={pl.title} - description={pl.description} - /> - ))} - - - -
{ - setData(() => data); - onExited(); - showSnackbar(t`Content model settings updated successfully.`); - }} - > - {({ Bind, submit, form, data: formData }) => ( - - -
- {typeof activePlugin.renderHeaderActions === "function" && - activePlugin.renderHeaderActions({ - Bind, - form, - formData - })} -
-
- - {activePlugin - ? activePlugin.render({ - Bind: Bind, - form, - formData - }) - : null} - - {activePlugin?.showSave !== false ? ( - -