diff --git a/cypress-tests/cypress/e2e/admin/pageBuilder/templates/pageTemplatesCrud.cy.ts b/cypress-tests/cypress/e2e/admin/pageBuilder/templates/pageTemplatesCrud.cy.ts index 0396446ebcf..4cbf3ebd593 100644 --- a/cypress-tests/cypress/e2e/admin/pageBuilder/templates/pageTemplatesCrud.cy.ts +++ b/cypress-tests/cypress/e2e/admin/pageBuilder/templates/pageTemplatesCrud.cy.ts @@ -34,7 +34,9 @@ context("Page Builder - Template CRUD", () => { }); }); cy.findByTestId("pb-editor-page-title").click(); - cy.get(`input[value="${templateName}"]`).clear().type(templateNameEdit).blur(); + cy.findByTestId("pb-editor-page-title").within(() => { + cy.get(`input[value="${templateName}"]`).clear().type(templateNameEdit).blur(); + }); cy.findByRole("button", { name: "Save Changes" }).should("exist").click(); cy.contains(templateNameEdit).should("exist"); diff --git a/packages/app-admin/src/hooks/index.ts b/packages/app-admin/src/hooks/index.ts index dcd3754347f..a3a404e6bfd 100644 --- a/packages/app-admin/src/hooks/index.ts +++ b/packages/app-admin/src/hooks/index.ts @@ -5,4 +5,5 @@ export * from "./useKeyHandler"; export * from "./useShiftKey"; export * from "./useModKey"; export * from "./useIsMounted"; +export * from "./useStateIfMounted"; export * from "./createGenericContext"; diff --git a/packages/app-admin/src/hooks/useStateIfMounted.ts b/packages/app-admin/src/hooks/useStateIfMounted.ts new file mode 100644 index 00000000000..8bb583e3504 --- /dev/null +++ b/packages/app-admin/src/hooks/useStateIfMounted.ts @@ -0,0 +1,20 @@ +import { useState, useCallback } from "react"; +import { useIsMounted } from "./useIsMounted"; + +export function useStateIfMounted( + defaultState: T +): [T, React.Dispatch>] { + const [state, setState] = useState(defaultState); + const { isMounted } = useIsMounted(); + + const setStateIfMounted = useCallback( + (state: React.SetStateAction) => { + if (isMounted()) { + setState(state); + } + }, + [isMounted] + ); + + return [state, setStateIfMounted]; +} diff --git a/packages/app-page-builder/src/PageBuilder.tsx b/packages/app-page-builder/src/PageBuilder.tsx index 58a12ff488d..a2bd7702c95 100644 --- a/packages/app-page-builder/src/PageBuilder.tsx +++ b/packages/app-page-builder/src/PageBuilder.tsx @@ -1,6 +1,7 @@ import React, { Fragment } from "react"; import { HasPermission } from "@webiny/app-security"; import { Plugins, AddMenu as Menu, createProviderPlugin, createDecorator } from "@webiny/app-admin"; +import { Global, css } from "@emotion/react"; import { PageBuilderProvider as ContextProvider } from "./contexts/PageBuilder"; import { ReactComponent as PagesIcon } from "./admin/assets/table_chart-24px.svg"; import { WebsiteSettings } from "./modules/WebsiteSettings/WebsiteSettings"; @@ -109,9 +110,17 @@ const EditorRendererPlugin = createDecorator(EditorRenderer, () => { }; }); +const displayContents = css` + pb-editor-elements, + pb-editor-element { + display: contents; + } +`; + export const PageBuilder = () => { return ( + diff --git a/packages/app-page-builder/src/admin/plugins/routes.tsx b/packages/app-page-builder/src/admin/plugins/routes.tsx index 153da2d2764..ab702c51081 100644 --- a/packages/app-page-builder/src/admin/plugins/routes.tsx +++ b/packages/app-page-builder/src/admin/plugins/routes.tsx @@ -95,7 +95,9 @@ const plugins: RoutePlugin[] = [ - + + + ); @@ -137,7 +139,9 @@ const plugins: RoutePlugin[] = [ - + + + ); @@ -195,7 +199,9 @@ const plugins: RoutePlugin[] = [ - + + + ); diff --git a/packages/app-page-builder/src/admin/views/Pages/PageTemplatesDialog.tsx b/packages/app-page-builder/src/admin/views/Pages/PageTemplatesDialog.tsx index e01a20fa176..9d4e01d3cbd 100644 --- a/packages/app-page-builder/src/admin/views/Pages/PageTemplatesDialog.tsx +++ b/packages/app-page-builder/src/admin/views/Pages/PageTemplatesDialog.tsx @@ -24,7 +24,7 @@ import { listStyle, TitleContent } from "./PageTemplatesDialogStyled"; -import * as Styled from "~/pageEditor/config/blockEditing/StyledComponents"; +import * as Styled from "~/templateEditor/config/Content/BlocksBrowser/StyledComponents"; import { PbPageTemplate } from "~/types"; const leftPanelStyle = css` diff --git a/packages/app-page-builder/src/blockEditor/Editor.tsx b/packages/app-page-builder/src/blockEditor/Editor.tsx index 36ffec4418d..53a43c82361 100644 --- a/packages/app-page-builder/src/blockEditor/Editor.tsx +++ b/packages/app-page-builder/src/blockEditor/Editor.tsx @@ -1,8 +1,8 @@ import React, { useMemo, useState } from "react"; import { useApolloClient } from "@apollo/react-hooks"; +import get from "lodash/get"; import { useRouter } from "@webiny/react-router"; import { plugins } from "@webiny/plugins"; -import get from "lodash/get"; import { Editor as PbEditor } from "~/admin/components/Editor"; import { EditorLoadingScreen } from "~/admin/components/EditorLoadingScreen"; import { @@ -12,6 +12,7 @@ import { } from "~/admin/graphql/pages"; import createElementPlugin from "~/admin/utils/createElementPlugin"; import { createStateInitializer } from "./createStateInitializer"; +import { DefaultEditorConfig } from "~/editor"; import { BlockEditorConfig } from "./config/BlockEditorConfig"; import { BlockWithContent } from "~/blockEditor/state"; import { createElement } from "~/editor/helpers"; @@ -67,8 +68,11 @@ export const BlockEditor = () => { return ( }> + + {/* PbEditor components is a shell component, which is decorated in `src/PageBuilder.tsx`. */} + {/* This allows developers to override how the editor component is loaded and mounted. */} diff --git a/packages/app-page-builder/src/blockEditor/components/elementSettingsTab/ElementNotLinked.tsx b/packages/app-page-builder/src/blockEditor/components/elementSettingsTab/ElementNotLinked.tsx index 08e9c20efa8..272630d6ceb 100644 --- a/packages/app-page-builder/src/blockEditor/components/elementSettingsTab/ElementNotLinked.tsx +++ b/packages/app-page-builder/src/blockEditor/components/elementSettingsTab/ElementNotLinked.tsx @@ -27,7 +27,7 @@ export const ElementLinkStatusWrapper = styled("div")({ } }); -const ElementNotLinked = () => { +export const ElementNotLinked = () => { return ( Element not linked @@ -44,5 +44,3 @@ const ElementNotLinked = () => { ); }; - -export default ElementNotLinked; diff --git a/packages/app-page-builder/src/blockEditor/components/elementSettingsTab/TextInput.tsx b/packages/app-page-builder/src/blockEditor/components/elementSettingsTab/TextInput.tsx index 9fa04a40955..1523f4dfea5 100644 --- a/packages/app-page-builder/src/blockEditor/components/elementSettingsTab/TextInput.tsx +++ b/packages/app-page-builder/src/blockEditor/components/elementSettingsTab/TextInput.tsx @@ -1,4 +1,5 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; +import { useStateIfMounted } from "@webiny/app-admin"; import { Input } from "@webiny/ui/Input"; interface TextInputProps { @@ -9,7 +10,7 @@ interface TextInputProps { } const TextInput = ({ label, value, onChange, onBlur }: TextInputProps) => { - const [localValue, setLocalValue] = useState(value); + const [localValue, setLocalValue] = useStateIfMounted(value); useEffect(() => { if (localValue !== value) { diff --git a/packages/app-page-builder/src/blockEditor/components/elementSettingsTab/VariablesList.tsx b/packages/app-page-builder/src/blockEditor/components/elementSettingsTab/VariablesList.tsx index b15498a6903..cadbe5ee9a3 100644 --- a/packages/app-page-builder/src/blockEditor/components/elementSettingsTab/VariablesList.tsx +++ b/packages/app-page-builder/src/blockEditor/components/elementSettingsTab/VariablesList.tsx @@ -4,10 +4,10 @@ import { PbEditorElement, PbBlockVariable, PbElement } from "~/types"; import { Typography } from "@webiny/ui/Typography"; import { useMoveVariable } from "~/blockEditor/components/elementSettingsTab/variablesListHooks"; import { useSortableList } from "~/hooks/useSortableList"; -import { Collapsable } from "~/editor/plugins/toolbar/navigator/StyledComponents"; +import { Collapsable } from "~/editor/defaultConfig/Toolbar/Navigator/StyledComponents"; import { useConfirmationDialog } from "@webiny/app-admin/hooks/useConfirmationDialog"; import { useUpdateElement } from "~/editor/hooks/useUpdateElement"; -import { ReactComponent as DragIndicatorIcon } from "~/editor/plugins/toolbar/navigator/assets/drag_indicator_24px.svg"; +import { ReactComponent as DragIndicatorIcon } from "~/editor/defaultConfig/Toolbar/Navigator/assets/drag_indicator_24px.svg"; import { ReactComponent as DeleteIcon } from "~/editor/assets/icons/delete.svg"; import { findElementByVariableId } from "~/blockEditor/config/eventActions/saveBlock"; import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; diff --git a/packages/app-page-builder/src/blockEditor/config/BlockEditorConfig.tsx b/packages/app-page-builder/src/blockEditor/config/BlockEditorConfig.tsx index e936a09b40b..8041d8b3e50 100644 --- a/packages/app-page-builder/src/blockEditor/config/BlockEditorConfig.tsx +++ b/packages/app-page-builder/src/blockEditor/config/BlockEditorConfig.tsx @@ -1,17 +1,15 @@ import React from "react"; import { EventActionPlugins, EventActionHandlerPlugin } from "./eventActions"; -import { EditorBarPlugins } from "./editorBar"; -import { ElementSettingsTabContentPlugin } from "./ElementSettingsTabContentPlugin"; -import { ToolbarActionsPlugin } from "./ToolbarActionsPlugin"; +import { DefaultBlockEditorConfig } from "./DefaultBlockEditorConfig"; +import { DefaultEditorConfig } from "~/editor"; export const BlockEditorConfig = React.memo(() => { return ( <> + - - - + ); }); diff --git a/packages/app-page-builder/src/blockEditor/config/DefaultBlockEditorConfig.tsx b/packages/app-page-builder/src/blockEditor/config/DefaultBlockEditorConfig.tsx new file mode 100644 index 00000000000..1f86d7b8512 --- /dev/null +++ b/packages/app-page-builder/src/blockEditor/config/DefaultBlockEditorConfig.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { BlockEditorConfig } from "~/blockEditor/editorConfig/BlockEditorConfig"; +import { BackButton } from "~/blockEditor/config/TopBar/BackButton"; +import { Title } from "~/blockEditor/config/TopBar/Title"; +import { SaveBlockButton } from "~/blockEditor/config/TopBar/SaveBlockButton"; +import { BlockSettingsButton } from "~/blockEditor/config/TopBar/BlockSettings/BlockSettingsButton"; +import { ElementSettingsDecorator } from "~/blockEditor/config/ElementSettingsTab"; + +const { TopBar, Toolbar } = BlockEditorConfig; + +export const DefaultBlockEditorConfig = () => { + return ( + + } /> + } /> + } /> + } + before={"buttonSaveBlock"} + /> + + + + ); +}; diff --git a/packages/app-page-builder/src/blockEditor/config/ElementSettingsTab.tsx b/packages/app-page-builder/src/blockEditor/config/ElementSettingsTab.tsx new file mode 100644 index 00000000000..1cd56387414 --- /dev/null +++ b/packages/app-page-builder/src/blockEditor/config/ElementSettingsTab.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { plugins } from "@webiny/plugins"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; +import { ElementNotLinked } from "~/blockEditor/components/elementSettingsTab/ElementNotLinked"; +import VariableSettings from "~/blockEditor/components/elementSettingsTab/VariableSettings"; +import VariablesList from "~/blockEditor/components/elementSettingsTab/VariablesList"; +import { PbBlockEditorCreateVariablePlugin } from "~/types"; +import { EditorConfig } from "~/editor/config"; + +export const ElementSettingsDecorator = EditorConfig.Sidebar.Elements.createDecorator(Original => { + const variablePlugins = plugins.byType( + "pb-block-editor-create-variable" + ); + + return function ElementGroup(props) { + const [element] = useActiveElement(); + + if (props.group !== "element") { + return ; + } + + const canHaveVariable = + element && variablePlugins.some(vp => vp.elementType === element.type); + const hasVariable = element && element.data?.variableId; + const isBlock = element && element.type === "block"; + + return ( + <> + {isBlock ? : } + {canHaveVariable && !hasVariable && } + {canHaveVariable && hasVariable && } + + ); + }; +}); diff --git a/packages/app-page-builder/src/blockEditor/config/ElementSettingsTabContentPlugin.tsx b/packages/app-page-builder/src/blockEditor/config/ElementSettingsTabContentPlugin.tsx deleted file mode 100644 index de53d248500..00000000000 --- a/packages/app-page-builder/src/blockEditor/config/ElementSettingsTabContentPlugin.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react"; -import { plugins } from "@webiny/plugins"; -import { SidebarActions } from "~/editor"; -import { createDecorator } from "@webiny/app-admin"; -import { useActiveElement } from "~/editor/hooks/useActiveElement"; -import ElementNotLinked from "~/blockEditor/components/elementSettingsTab/ElementNotLinked"; -import VariableSettings from "~/blockEditor/components/elementSettingsTab/VariableSettings"; -import VariablesList from "~/blockEditor/components/elementSettingsTab/VariablesList"; -import { PbBlockEditorCreateVariablePlugin } from "~/types"; - -export const ElementSettingsTabContentPlugin = createDecorator( - SidebarActions, - SidebarActionsWrapper => { - const variablePlugins = plugins.byType( - "pb-block-editor-create-variable" - ); - - return function SettingsTabContent({ children, ...props }) { - const [element] = useActiveElement(); - const canHaveVariable = - element && - variablePlugins.some(variablePlugin => variablePlugin.elementType === element.type); - const hasVariable = element && element.data?.variableId; - const isBlock = element && element.type === "block"; - - return ( - <> - {isBlock ? ( - - ) : ( - {children} - )} - {canHaveVariable && !hasVariable && } - {canHaveVariable && hasVariable && } - - ); - }; - } -); diff --git a/packages/app-page-builder/src/blockEditor/config/ToolbarActionsPlugin.tsx b/packages/app-page-builder/src/blockEditor/config/ToolbarActionsPlugin.tsx deleted file mode 100644 index 38fd039fc49..00000000000 --- a/packages/app-page-builder/src/blockEditor/config/ToolbarActionsPlugin.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react"; -import { ToolbarActions } from "~/editor"; -import { createDecorator } from "@webiny/app-admin"; -import { plugins } from "@webiny/plugins"; -import { renderPlugin } from "~/editor/components/Editor/Toolbar"; -import { PbEditorToolbarBottomPlugin, PbEditorToolbarTopPlugin } from "~/types"; - -export const ToolbarActionsPlugin = createDecorator(ToolbarActions, ToolbarActionsWrapper => { - return function BlockEditorToolbarActions() { - const actionsTop = plugins.byType("pb-editor-toolbar-top"); - const actionsBottom = plugins - .byType("pb-editor-toolbar-bottom") - .filter(plugin => plugin.name !== "pb-editor-toolbar-save"); - - return ( - -
{actionsTop.map(renderPlugin)}
-
{actionsBottom.map(renderPlugin)}
-
- ); - }; -}); diff --git a/packages/app-page-builder/src/blockEditor/config/TopBar/BackButton/BackButton.tsx b/packages/app-page-builder/src/blockEditor/config/TopBar/BackButton/BackButton.tsx new file mode 100644 index 00000000000..f460d9dad35 --- /dev/null +++ b/packages/app-page-builder/src/blockEditor/config/TopBar/BackButton/BackButton.tsx @@ -0,0 +1,38 @@ +import React, { useCallback } from "react"; +import { css } from "emotion"; +import { useLocation, useNavigate } from "@webiny/react-router"; +import { IconButton } from "@webiny/ui/Button"; +import { ReactComponent as BackIcon } from "./round-arrow_back-24px.svg"; +import { useBlock } from "~/blockEditor/hooks/useBlock"; +import { BlockEditorConfig } from "~/blockEditor/editorConfig/BlockEditorConfig"; + +const backStyles = css({ + marginLeft: -10 +}); + +export function BackButton() { + const { key } = useLocation(); + const navigate = useNavigate(); + const [block] = useBlock(); + + const onClick = useCallback(() => { + // If location.key is "default", then we are in a new tab. + if (key === "default") { + navigate(`/page-builder/page-blocks?category=${block.blockCategory}`); + } else { + navigate(-1); + } + }, [key, navigate]); + + return ( + <> + } + /> + + + ); +} diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/BackButton/index.ts b/packages/app-page-builder/src/blockEditor/config/TopBar/BackButton/index.ts similarity index 100% rename from packages/app-page-builder/src/blockEditor/config/editorBar/BackButton/index.ts rename to packages/app-page-builder/src/blockEditor/config/TopBar/BackButton/index.ts diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/BackButton/round-arrow_back-24px.svg b/packages/app-page-builder/src/blockEditor/config/TopBar/BackButton/round-arrow_back-24px.svg similarity index 100% rename from packages/app-page-builder/src/blockEditor/config/editorBar/BackButton/round-arrow_back-24px.svg rename to packages/app-page-builder/src/blockEditor/config/TopBar/BackButton/round-arrow_back-24px.svg diff --git a/packages/app-page-builder/src/blockEditor/config/TopBar/BlockSettings/BlockSettingsButton.tsx b/packages/app-page-builder/src/blockEditor/config/TopBar/BlockSettings/BlockSettingsButton.tsx new file mode 100644 index 00000000000..c9ac0b83043 --- /dev/null +++ b/packages/app-page-builder/src/blockEditor/config/TopBar/BlockSettings/BlockSettingsButton.tsx @@ -0,0 +1,23 @@ +import React, { useCallback, useState } from "react"; +import { IconButton } from "@webiny/ui/Button"; +import { ReactComponent as SettingsIcon } from "./settings.svg"; +import { BlockSettingsModal } from "./BlockSettingsModal"; + +export const BlockSettingsButton = () => { + const [open, setState] = useState(false); + + const onClickHandler = useCallback(() => { + setState(true); + }, []); + + const onClose = useCallback(() => { + setState(false); + }, []); + + return ( + <> + } /> + + + ); +}; diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/BlockSettingsModal.tsx b/packages/app-page-builder/src/blockEditor/config/TopBar/BlockSettings/BlockSettingsModal.tsx similarity index 71% rename from packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/BlockSettingsModal.tsx rename to packages/app-page-builder/src/blockEditor/config/TopBar/BlockSettings/BlockSettingsModal.tsx index 318a69855ec..d9847e9501f 100644 --- a/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/BlockSettingsModal.tsx +++ b/packages/app-page-builder/src/blockEditor/config/TopBar/BlockSettings/BlockSettingsModal.tsx @@ -1,35 +1,25 @@ import React, { useCallback } from "react"; -import styled from "@emotion/styled"; -import { useRecoilState } from "recoil"; - import { Form } from "@webiny/form"; import { ButtonPrimary } from "@webiny/ui/Button"; import { SimpleFormContent } from "@webiny/app-admin/components/SimpleForm"; import { validation } from "@webiny/validation"; import { Select } from "@webiny/ui/Select"; import { Dialog, DialogCancel, DialogTitle, DialogActions, DialogContent } from "@webiny/ui/Dialog"; - -import { blockSettingsStateAtom } from "./state"; import { useBlock } from "~/blockEditor/hooks/useBlock"; import { useBlockCategories } from "~/blockEditor/hooks/useBlockCategories"; import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; import { UpdateDocumentActionEvent } from "~/editor/recoil/actions"; import { BlockAtomType } from "~/blockEditor/state"; -const ButtonWrapper = styled("div")({ - display: "flex", - justifyContent: "space-between", - width: "100%" -}); +export interface BlockSettingsModalProps { + open: boolean; + onClose: () => void; +} -const BlockSettingsModal = () => { +export const BlockSettingsModal = ({ open, onClose }: BlockSettingsModalProps) => { const handler = useEventActionHandler(); const [block] = useBlock(); const blockCategories = useBlockCategories(); - const [, setState] = useRecoilState(blockSettingsStateAtom); - const onClose = useCallback(() => { - setState(false); - }, []); const updateBlock = (data: Partial) => { handler.trigger( @@ -46,7 +36,7 @@ const BlockSettingsModal = () => { }, []); return ( - +
{({ form, Bind }) => ( <> @@ -68,16 +58,14 @@ const BlockSettingsModal = () => { - - Cancel - { - form.submit(ev); - }} - > - Save - - + Cancel + { + form.submit(ev); + }} + > + Save + )} @@ -85,5 +73,3 @@ const BlockSettingsModal = () => {
); }; - -export default BlockSettingsModal; diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/settings.svg b/packages/app-page-builder/src/blockEditor/config/TopBar/BlockSettings/settings.svg similarity index 100% rename from packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/settings.svg rename to packages/app-page-builder/src/blockEditor/config/TopBar/BlockSettings/settings.svg diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/SaveBlockButton/SaveBlockButton.tsx b/packages/app-page-builder/src/blockEditor/config/TopBar/SaveBlockButton/SaveBlockButton.tsx similarity index 85% rename from packages/app-page-builder/src/blockEditor/config/editorBar/SaveBlockButton/SaveBlockButton.tsx rename to packages/app-page-builder/src/blockEditor/config/TopBar/SaveBlockButton/SaveBlockButton.tsx index 5e5f1021091..2e102070aa2 100644 --- a/packages/app-page-builder/src/blockEditor/config/editorBar/SaveBlockButton/SaveBlockButton.tsx +++ b/packages/app-page-builder/src/blockEditor/config/TopBar/SaveBlockButton/SaveBlockButton.tsx @@ -1,11 +1,9 @@ import React, { useCallback, useState } from "react"; import styled from "@emotion/styled"; import { useLocation, useNavigate } from "@webiny/react-router"; -import { createDecorator, makeDecoratable } from "@webiny/app-admin"; import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar"; import { ButtonIcon, ButtonPrimary } from "@webiny/ui/Button"; import { CircularProgress } from "@webiny/ui/Progress"; -import { EditorBar } from "~/editor"; import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; import { useBlock } from "~/blockEditor/hooks/useBlock"; import { SaveBlockActionEvent } from "~/blockEditor/config/eventActions/saveBlock/event"; @@ -20,7 +18,7 @@ const SpinnerWrapper = styled.div` margin-left: -4px !important; `; -const DefaultSaveBlockButton = () => { +export const SaveBlockButton = () => { const [block] = useBlock(); const eventActionHandler = useEventActionHandler(); const { key } = useLocation(); @@ -81,16 +79,3 @@ const DefaultSaveBlockButton = () => { ); }; - -export const SaveBlockButton = makeDecoratable("SaveBlockButton", DefaultSaveBlockButton); - -export const SaveBlockButtonPlugin = createDecorator(EditorBar.RightSection, RightSection => { - return function AddSaveBlockButton(props) { - return ( - - - {props.children} - - ); - }; -}); diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/SaveBlockButton/graphql.ts b/packages/app-page-builder/src/blockEditor/config/TopBar/SaveBlockButton/graphql.ts similarity index 100% rename from packages/app-page-builder/src/blockEditor/config/editorBar/SaveBlockButton/graphql.ts rename to packages/app-page-builder/src/blockEditor/config/TopBar/SaveBlockButton/graphql.ts diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/SaveBlockButton/index.ts b/packages/app-page-builder/src/blockEditor/config/TopBar/SaveBlockButton/index.ts similarity index 100% rename from packages/app-page-builder/src/blockEditor/config/editorBar/SaveBlockButton/index.ts rename to packages/app-page-builder/src/blockEditor/config/TopBar/SaveBlockButton/index.ts diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/Title/Styled.ts b/packages/app-page-builder/src/blockEditor/config/TopBar/Title/Styled.ts similarity index 100% rename from packages/app-page-builder/src/blockEditor/config/editorBar/Title/Styled.ts rename to packages/app-page-builder/src/blockEditor/config/TopBar/Title/Styled.ts diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/Title/Title.tsx b/packages/app-page-builder/src/blockEditor/config/TopBar/Title/Title.tsx similarity index 89% rename from packages/app-page-builder/src/blockEditor/config/editorBar/Title/Title.tsx rename to packages/app-page-builder/src/blockEditor/config/TopBar/Title/Title.tsx index 662c037f4a0..ffa619e8d8d 100644 --- a/packages/app-page-builder/src/blockEditor/config/editorBar/Title/Title.tsx +++ b/packages/app-page-builder/src/blockEditor/config/TopBar/Title/Title.tsx @@ -2,12 +2,10 @@ import React, { useState, useCallback, SyntheticEvent } from "react"; import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar"; import { Input } from "@webiny/ui/Input"; import { Tooltip } from "@webiny/ui/Tooltip"; -import { createDecorator } from "@webiny/app-admin"; import { BlockTitle, blockTitleWrapper, TitleInputWrapper, TitleWrapper } from "./Styled"; import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; import { BlockAtomType } from "~/blockEditor/state"; import { UpdateDocumentActionEvent } from "~/editor/recoil/actions"; -import { EditorBar } from "~/editor"; import { useBlock } from "~/blockEditor/hooks/useBlock"; declare global { @@ -16,7 +14,7 @@ declare global { } } -const Title = () => { +export const Title = () => { const handler = useEventActionHandler(); const [block] = useBlock(); const { showSnackbar } = useSnackbar(); @@ -86,7 +84,7 @@ const Title = () => { const autoFocus = !window.Cypress; return editTitle ? ( - + { ); }; - -export const TitlePlugin = createDecorator(EditorBar.LeftSection, LeftSection => { - return function AddTitle(props) { - return ( - - {props.children} - - </LeftSection> - ); - }; -}); diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/Title/index.ts b/packages/app-page-builder/src/blockEditor/config/TopBar/Title/index.ts similarity index 100% rename from packages/app-page-builder/src/blockEditor/config/editorBar/Title/index.ts rename to packages/app-page-builder/src/blockEditor/config/TopBar/Title/index.ts diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/BackButton/BackButton.tsx b/packages/app-page-builder/src/blockEditor/config/editorBar/BackButton/BackButton.tsx deleted file mode 100644 index 804078cfb2b..00000000000 --- a/packages/app-page-builder/src/blockEditor/config/editorBar/BackButton/BackButton.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { useCallback } from "react"; -import { css } from "emotion"; -import { createDecorator } from "@webiny/app-admin"; -import { useLocation, useNavigate } from "@webiny/react-router"; -import { IconButton } from "@webiny/ui/Button"; -import { EditorBar } from "~/editor"; -import { ReactComponent as BackIcon } from "./round-arrow_back-24px.svg"; -import { useBlock } from "~/blockEditor/hooks/useBlock"; - -const backStyles = css({ - marginLeft: -10 -}); - -export const BackButtonPlugin = createDecorator(EditorBar.BackButton, () => { - return function BackButton() { - const { key } = useLocation(); - const navigate = useNavigate(); - const [block] = useBlock(); - - const onClick = useCallback(() => { - // If location.key is "default", then we are in a new tab. - if (key === "default") { - navigate(`/page-builder/page-blocks?category=${block.blockCategory}`); - } else { - navigate(-1); - } - }, [key, navigate]); - - return ( - <IconButton - data-testid="pb-editor-back-button" - className={backStyles} - onClick={onClick} - icon={<BackIcon />} - /> - ); - }; -}); diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/BlockSettings.tsx b/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/BlockSettings.tsx deleted file mode 100644 index e5865aa0e4b..00000000000 --- a/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/BlockSettings.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; -import { createDecorator } from "@webiny/app-admin"; -import { blockSettingsStateAtom } from "./state"; -import { useRecoilValue } from "recoil"; -import BlockSettingsModal from "./BlockSettingsModal"; - -/* For the time being, we're importing from the base editor, to not break things for existing users. */ -import { EditorBar } from "~/editor"; - -export const BlockSettingsOverlay = createDecorator(EditorBar, EditorBar => { - return function BlockSettingsOverlay() { - const isActive = useRecoilValue(blockSettingsStateAtom); - - return ( - <> - <EditorBar /> - {isActive ? <BlockSettingsModal /> : null} - </> - ); - }; -}); diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/BlockSettingsButton.tsx b/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/BlockSettingsButton.tsx deleted file mode 100644 index 87ee319bf52..00000000000 --- a/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/BlockSettingsButton.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { useCallback } from "react"; -import { useRecoilState } from "recoil"; -import { IconButton } from "@webiny/ui/Button"; -import { createDecorator } from "@webiny/app-admin"; -import { ReactComponent as SettingsIcon } from "./settings.svg"; -import { blockSettingsStateAtom } from "./state"; -import { EditorBar } from "~/editor"; - -const BlockSettingsButton = () => { - const [, setState] = useRecoilState(blockSettingsStateAtom); - const onClickHandler = useCallback(() => { - setState(true); - }, []); - - return <IconButton onClick={onClickHandler} icon={<SettingsIcon />} />; -}; - -export const AddBlockSettingsButton = createDecorator(EditorBar.RightSection, RightSection => { - return function ComposeRightSection(props) { - return ( - <RightSection> - <BlockSettingsButton /> - {props.children} - </RightSection> - ); - }; -}); diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/index.tsx b/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/index.tsx deleted file mode 100644 index 97c24bb7a79..00000000000 --- a/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import { BlockSettingsOverlay } from "./BlockSettings"; -import { AddBlockSettingsButton } from "./BlockSettingsButton"; - -export const BlockSettingsPlugin = () => { - return ( - <> - <BlockSettingsOverlay /> - <AddBlockSettingsButton /> - </> - ); -}; diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/state.ts b/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/state.ts deleted file mode 100644 index 0aa0e1029e0..00000000000 --- a/packages/app-page-builder/src/blockEditor/config/editorBar/BlockSettings/state.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { atom } from "recoil"; - -export type BlockSettingsState = boolean; - -export const blockSettingsStateAtom = atom<BlockSettingsState>({ - key: "blockSettingsStateAtom", - default: false -}); diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/EditorBarPlugins.tsx b/packages/app-page-builder/src/blockEditor/config/editorBar/EditorBarPlugins.tsx deleted file mode 100644 index f8a9b644f52..00000000000 --- a/packages/app-page-builder/src/blockEditor/config/editorBar/EditorBarPlugins.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import { BackButtonPlugin } from "./BackButton"; -import { BlockSettingsPlugin } from "./BlockSettings"; -import { SaveBlockButtonPlugin } from "./SaveBlockButton"; -import { TitlePlugin } from "./Title"; - -export const EditorBarPlugins = () => { - return ( - <> - <BackButtonPlugin /> - <TitlePlugin /> - <BlockSettingsPlugin /> - <SaveBlockButtonPlugin /> - </> - ); -}; diff --git a/packages/app-page-builder/src/blockEditor/config/editorBar/index.ts b/packages/app-page-builder/src/blockEditor/config/editorBar/index.ts deleted file mode 100644 index b01e258dbc0..00000000000 --- a/packages/app-page-builder/src/blockEditor/config/editorBar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { EditorBarPlugins } from "./EditorBarPlugins"; diff --git a/packages/app-page-builder/src/blockEditor/editorConfig/BlockEditorConfig.tsx b/packages/app-page-builder/src/blockEditor/editorConfig/BlockEditorConfig.tsx new file mode 100644 index 00000000000..7f3d9aa5542 --- /dev/null +++ b/packages/app-page-builder/src/blockEditor/editorConfig/BlockEditorConfig.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { CompositionScope } from "@webiny/app-admin"; +import { EditorConfig } from "~/editor/config"; + +interface BlockEditorConfigProps { + children: React.ReactNode; +} + +const BaseBlockEditorConfig = ({ children }: BlockEditorConfigProps) => { + return ( + <CompositionScope name={"pb.blockEditor"}> + <EditorConfig>{children}</EditorConfig> + </CompositionScope> + ); +}; + +export const BlockEditorConfig = Object.assign(BaseBlockEditorConfig, EditorConfig); diff --git a/packages/app-page-builder/src/components.ts b/packages/app-page-builder/src/components.ts index dcc91475b69..e7b3201f2f7 100644 --- a/packages/app-page-builder/src/components.ts +++ b/packages/app-page-builder/src/components.ts @@ -13,7 +13,6 @@ import { } from "~/admin/plugins/pageDetails/pageRevisions/MenuOptions"; import { PageRevisionListItemGraphic } from "~/admin/plugins/pageDetails/pageRevisions/PageRevisionListItemGraphic"; import { PublishPageButton, usePage as usePageFromEditor } from "./pageEditor"; -import { PreviewPage as PreviewPageMenuOption } from "./pageEditor/config/editorBar/PreviewPageButton/PreviewPageButton"; import { DeletePage, MovePage, @@ -132,11 +131,7 @@ export const Components = { /** * This component renders a button to publish the page. */ - PublishPage: PublishPageButton, - /** - * This component renders an item in the dropdown menu. - */ - PreviewPage: PreviewPageMenuOption + PublishPage: PublishPageButton } } }; diff --git a/packages/app-page-builder/src/editor/Editor.tsx b/packages/app-page-builder/src/editor/Editor.tsx index 3f874ed7ee7..ed8c5c64357 100644 --- a/packages/app-page-builder/src/editor/Editor.tsx +++ b/packages/app-page-builder/src/editor/Editor.tsx @@ -1,8 +1,9 @@ /** * This file contains the base framework for building editor variations. - * Currently, we have 2 editors: - * - page editor + * Currently, we have 3 editors: * - block editor + * - template editor + * - page editor * * This framework provides the basic mechanics, like d&d elements, element settings, toolbars, etc. * Other things, like loading/saving data to and from the GraphQL API, toolbar elements, etc. need @@ -13,12 +14,12 @@ import { RecoilRoot, MutableSnapshot } from "recoil"; import { Editor as EditorComponent } from "./components/Editor"; import { EditorConfigApply } from "./components/Editor/EditorConfig"; import { EditorProvider } from "./contexts/EditorProvider"; -import { EditorDefaultConfig } from "./config/EditorDefaultConfig"; import { HTML5Backend } from "react-dnd-html5-backend"; import { DndProvider } from "react-dnd"; import { elementsAtom, rootElementAtom } from "~/editor/recoil/modules"; import { flattenElements } from "~/editor/helpers"; import { PbEditorElement } from "~/types"; +import { EditorWithConfig } from "~/editor/config"; export interface EditorStateInitializerFactory { (): EditorStateInitializer; @@ -55,9 +56,10 @@ export const Editor = ({ stateInitializerFactory }: EditorProps) => { <DndProvider backend={HTML5Backend}> <RecoilRoot initializeState={initializeState}> <EditorProvider> - <EditorDefaultConfig /> <EditorConfigApply /> - <EditorComponent /> + <EditorWithConfig> + <EditorComponent /> + </EditorWithConfig> </EditorProvider> </RecoilRoot> </DndProvider> diff --git a/packages/app-page-builder/src/editor/components/Editor/Editor.tsx b/packages/app-page-builder/src/editor/components/Editor/Editor.tsx index a78c08f1531..d2dea7547c1 100644 --- a/packages/app-page-builder/src/editor/components/Editor/Editor.tsx +++ b/packages/app-page-builder/src/editor/components/Editor/Editor.tsx @@ -1,19 +1,14 @@ import React, { useEffect } from "react"; import classSet from "classnames"; +import { plugins } from "@webiny/plugins"; import { useEventActionHandler } from "../../hooks/useEventActionHandler"; import { EventActionHandler, PbEditorEventActionPlugin } from "~/types"; -import { rootElementAtom, uiAtom } from "../../recoil/modules"; -import { useRecoilValue } from "recoil"; -import { useKeyHandler } from "../../hooks/useKeyHandler"; -import { plugins } from "@webiny/plugins"; +import { useKeyHandler } from "~/editor/hooks/useKeyHandler"; import "./Editor.scss"; -// Components -import { EditorBar } from "~/editor"; -import EditorToolbar from "./Toolbar"; -import EditorContent from "./Content"; import DragPreview from "./DragPreview"; import Dialogs from "./Dialogs"; -import { EditorSidebar } from "./EditorSidebar"; +import { useUI } from "~/editor/hooks/useUI"; +import { EditorConfig } from "~/editor/config"; type PluginRegistryType = Map<string, () => void>; @@ -52,34 +47,21 @@ const unregisterPlugins = (handler: EventActionHandler, registered: PluginRegist } }; -const triggerActionButtonClick = (name: string): void => { - const id = `#action-${name}`; - const element = document.querySelector<HTMLElement>(id); - if (!element) { - console.warn(`There is no html element "${id}"`); - return; - } - element.click(); -}; - export const Editor = () => { const eventActionHandler = useEventActionHandler(); const { addKeyHandler, removeKeyHandler } = useKeyHandler(); - const { isDragging, isResizing } = useRecoilValue(uiAtom); + const [{ isDragging, isResizing }] = useUI(); - const rootElementId = useRecoilValue(rootElementAtom); - - const firstRender = React.useRef<boolean>(true); const registeredPlugins = React.useRef<PluginRegistryType>(new Map()); useEffect(() => { addKeyHandler("mod+z", e => { e.preventDefault(); - triggerActionButtonClick("undo"); + eventActionHandler.undo(); }); addKeyHandler("mod+shift+z", e => { e.preventDefault(); - triggerActionButtonClick("redo"); + eventActionHandler.redo(); }); registeredPlugins.current = registerPlugins(eventActionHandler); @@ -91,13 +73,6 @@ export const Editor = () => { }; }, []); - useEffect(() => { - if (!rootElementId || firstRender.current === true) { - firstRender.current = false; - return; - } - }, [rootElementId]); - const classes = { "pb-editor": true, "pb-editor-dragging": isDragging, @@ -105,10 +80,7 @@ export const Editor = () => { }; return ( <div className={classSet(classes)}> - <EditorBar /> - <EditorToolbar /> - <EditorContent /> - <EditorSidebar /> + <EditorConfig.Layout /> <Dialogs /> <DragPreview /> </div> diff --git a/packages/app-page-builder/src/editor/components/Editor/EditorBar.tsx b/packages/app-page-builder/src/editor/components/Editor/EditorBar.tsx index 9ee931b4fa0..1dcb4dccfb4 100644 --- a/packages/app-page-builder/src/editor/components/Editor/EditorBar.tsx +++ b/packages/app-page-builder/src/editor/components/Editor/EditorBar.tsx @@ -1,12 +1,8 @@ import React from "react"; import { css } from "emotion"; import styled from "@emotion/styled"; -import { TopAppBar, TopAppBarSection } from "@webiny/ui/TopAppBar"; -import { createVoidComponent, DecoratableComponent, makeDecoratable } from "@webiny/app-admin"; - -const topBar = css({ - boxShadow: "1px 0px 5px 0px rgba(128,128,128,1)" -}); +import { TopAppBarSection } from "@webiny/ui/TopAppBar"; +import { createVoidComponent, makeDecoratable } from "@webiny/app-admin"; const centerTopBar = css({ "&.mdc-top-app-bar__section": { @@ -61,32 +57,13 @@ const DividerRenderer = makeDecoratable("DividerRenderer", () => { return <StyledDivider />; }); -type EditorBar = DecoratableComponent & { - LeftSection: DecoratableComponent; - CenterSection: DecoratableComponent; - RightSection: DecoratableComponent; - BackButton: DecoratableComponent; - Divider: DecoratableComponent; -}; - -const ComposableEditorBar = makeDecoratable("EditorBar", () => { - return <EditorBarRenderer />; -}); - -export const EditorBarRenderer = makeDecoratable("EditorBarRenderer", () => { - return ( - <TopAppBar className={topBar} fixed> - <LeftSection /> - <CenterSection /> - <RightSection /> - </TopAppBar> - ); -}); - -export const EditorBar: EditorBar = Object.assign(ComposableEditorBar, { - LeftSection, - CenterSection, - RightSection, - BackButton, - Divider -}); +export const EditorBar = Object.assign( + {}, + { + LeftSection, + CenterSection, + RightSection, + BackButton, + Divider + } +); diff --git a/packages/app-page-builder/src/editor/components/Editor/EditorContent.tsx b/packages/app-page-builder/src/editor/components/Editor/EditorContent.tsx deleted file mode 100644 index 675a4742d05..00000000000 --- a/packages/app-page-builder/src/editor/components/Editor/EditorContent.tsx +++ /dev/null @@ -1,12 +0,0 @@ -/** - * This file serves as a placeholder for central editor content. - * Previously, this was done via `pb-editor-content` plugins. - */ -import React from "react"; -import { createVoidComponent, makeDecoratable } from "@webiny/app-admin"; - -export const EditorContent = makeDecoratable("EditorContent", () => { - return <EditorContentRenderer />; -}); - -const EditorContentRenderer = makeDecoratable("EditorContentRenderer", createVoidComponent()); diff --git a/packages/app-page-builder/src/editor/components/Editor/EditorSidebar.tsx b/packages/app-page-builder/src/editor/components/Editor/EditorSidebar.tsx deleted file mode 100644 index 31b4d42c88b..00000000000 --- a/packages/app-page-builder/src/editor/components/Editor/EditorSidebar.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React, { useCallback, useEffect } from "react"; -import styled from "@emotion/styled"; -import { css } from "emotion"; -import store from "store"; -import { makeDecoratable } from "@webiny/app-admin"; -import { Elevation } from "@webiny/ui/Elevation"; -import { Tabs, Tab, TabProps } from "@webiny/ui/Tabs"; -import { - highlightSidebarTabMutation, - updateSidebarActiveTabIndexMutation -} from "~/editor/recoil/modules"; -import StyleSettingsTabContent from "./Sidebar/StyleSettingsTabContent"; -import ElementSettingsTabContent from "./Sidebar/ElementSettingsTabContent"; -import { useActiveElement } from "~/editor/hooks/useActiveElement"; -import { useElementSidebar } from "~/editor/hooks/useElementSidebar"; - -const LOCAL_STORAGE_KEY = "webiny_pb_editor_active_tab"; - -const rightSideBar = css({ - boxShadow: "1px 0px 5px 0px rgba(128,128,128,1)", - position: "fixed", - right: 0, - top: 65, - height: "100%", - width: 300, - zIndex: 1 -}); - -const PanelHighLight = styled("div")({ - "&": { - opacity: 0, - animation: "wf-blink-in 1s", - border: "2px solid var(--mdc-theme-secondary)", - boxShadow: "0 0 15px var(--mdc-theme-secondary)", - backgroundColor: "rgba(42, 217, 134, 0.25)", - borderRadius: "2px", - position: "absolute", - top: "0", - left: "0", - right: "0", - bottom: "0", - zIndex: 1, - pointerEvents: "none" - }, - "@keyframes wf-blink-in": { "40%": { opacity: 1 } } -}); - -export const EditorSidebar = React.memo(() => { - const [element] = useActiveElement(); - const [sidebar, setSidebar] = useElementSidebar(); - - const activeTabIndex = store.get(LOCAL_STORAGE_KEY, sidebar.activeTabIndex) ?? 0; - - const setActiveTabIndex = useCallback( - index => { - setSidebar(prev => updateSidebarActiveTabIndexMutation(prev, index)); - if (element) { - store.set(LOCAL_STORAGE_KEY, index); - } - }, - [element] - ); - - const unhighlightElementTab = useCallback(() => { - setSidebar(prev => highlightSidebarTabMutation(prev, false)); - }, []); - - useEffect(() => { - if (sidebar.highlightTab) { - setTimeout(unhighlightElementTab, 1000); - } - }, [sidebar.highlightTab]); - - return ( - <Elevation z={1} className={rightSideBar}> - <Tabs value={activeTabIndex} onActivate={setActiveTabIndex}> - <EditorSidebarTab label={"Style"}> - <StyleSettingsTabContent /> - </EditorSidebarTab> - <EditorSidebarTab label={"Element"} disabled={!element}> - <ElementSettingsTabContent /> - </EditorSidebarTab> - </Tabs> - {sidebar.highlightTab && <PanelHighLight />} - </Elevation> - ); -}); - -EditorSidebar.displayName = "EditorSidebar"; - -export type EditorSidebarTabProps = TabProps; - -export const EditorSidebarTab = makeDecoratable( - "EditorSidebarTab", - ({ children, ...props }: EditorSidebarTabProps): JSX.Element | null => { - return <Tab {...props}>{children}</Tab>; - } -); diff --git a/packages/app-page-builder/src/editor/components/Editor/Sidebar/ElementSettingsTabContent.tsx b/packages/app-page-builder/src/editor/components/Editor/Sidebar/ElementSettingsTabContent.tsx deleted file mode 100644 index 976d94853b9..00000000000 --- a/packages/app-page-builder/src/editor/components/Editor/Sidebar/ElementSettingsTabContent.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from "react"; -import styled from "@emotion/styled"; -import { makeDecoratable } from "@webiny/app-admin"; -import NoActiveElement from "./NoActiveElement"; -import { ReactComponent as TouchIcon } from "./icons/touch_app.svg"; -import { useActiveElement } from "~/editor/hooks/useActiveElement"; -import { COLORS } from "~/editor/plugins/elementSettings/components/StyledComponents"; -import useElementSettings from "~/editor/plugins/elementSettings/hooks/useElementSettings"; -import { ElementSettings } from "~/editor/plugins/elementSettings/advanced/ElementSettings"; - -export const RootElement = styled("div")({ - display: "flex", - flexDirection: "column", - height: "calc(100vh - 65px - 48px)", // Subtract top-bar and tab-header height - overflowY: "auto", - // Style scrollbar - "&::-webkit-scrollbar": { - width: 1 - }, - "&::-webkit-scrollbar-track": { - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)" - }, - "&::-webkit-scrollbar-thumb": { - backgroundColor: "darkgrey", - outline: "1px solid slategrey" - } -}); - -const SidebarActionsWrapper = styled("div")({ - display: "flex", - flexWrap: "wrap", - borderBottom: `1px solid ${COLORS.gray}`, - backgroundColor: COLORS.lightGray, - borderTop: `1px solid ${COLORS.gray}`, - justifyContent: "center" -}); - -const ElementSettingsTabContent = () => { - const [element] = useActiveElement(); - const elementSettings = useElementSettings(); - - if (!element) { - return ( - <NoActiveElement - icon={<TouchIcon />} - message={"Select an element on the canvas to activate this panel."} - /> - ); - } - - return ( - <RootElement> - <SidebarActions> - {elementSettings.map(({ plugin, options }, index) => { - return ( - <div key={plugin.name + "-" + index}> - {typeof plugin.renderAction === "function" && - plugin.renderAction({ options })} - </div> - ); - })} - </SidebarActions> - </RootElement> - ); -}; - -export const SidebarActions = makeDecoratable( - "ElementSettingsTabContent", - ({ children, ...props }) => { - return ( - <> - <SidebarActionsWrapper {...props}>{children}</SidebarActionsWrapper> - <ElementSettings /> - </> - ); - } -); - -export default React.memo(ElementSettingsTabContent); diff --git a/packages/app-page-builder/src/editor/components/Editor/Sidebar/NoActiveElement.tsx b/packages/app-page-builder/src/editor/components/Editor/Sidebar/NoActiveElement.tsx deleted file mode 100644 index 3730de40272..00000000000 --- a/packages/app-page-builder/src/editor/components/Editor/Sidebar/NoActiveElement.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { ReactElement } from "react"; -import { css } from "emotion"; -import { Typography } from "@webiny/ui/Typography"; - -const noActiveElementWrapper = css({ - padding: 16, - display: "flex", - flexDirection: "column", - justifyContent: "center", - alignItems: "center", - margin: "48px 16px", - backgroundColor: "var(--mdc-theme-background)", - color: "var(--mdc-theme-text-primary-on-background)", - maxHeight: 150, - "& .icon": { - fill: "var(--mdc-theme-text-icon-on-background)", - width: 36, - height: 36 - }, - "& .text": { - marginTop: 16, - color: "var(--mdc-theme-text-on-background)", - textAlign: "center" - } -}); - -interface NoActiveElementProps { - message: string; - icon?: ReactElement; -} - -const NoActiveElement = ({ message, icon }: NoActiveElementProps) => { - return ( - <div className={noActiveElementWrapper}> - {icon && React.cloneElement(icon, { className: "icon" })} - <Typography use={"subtitle1"} className={"text"}> - {message} - </Typography> - </div> - ); -}; - -export default React.memo(NoActiveElement); diff --git a/packages/app-page-builder/src/editor/components/Editor/Sidebar/StyleSettingsTabContent.tsx b/packages/app-page-builder/src/editor/components/Editor/Sidebar/StyleSettingsTabContent.tsx deleted file mode 100644 index ac65f16e4ae..00000000000 --- a/packages/app-page-builder/src/editor/components/Editor/Sidebar/StyleSettingsTabContent.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from "react"; -import styled from "@emotion/styled"; -import NoActiveElement from "./NoActiveElement"; -import { ReactComponent as TouchIcon } from "./icons/touch_app.svg"; -import { ReactComponent as WarningIcon } from "./icons/warning-black.svg"; -import { useActiveElement } from "~/editor/hooks/useActiveElement"; -import useElementStyleSettings from "~/editor/plugins/elementSettings/hooks/useElementStyleSettings"; - -const RootElement = styled("div")({ - height: "calc(100vh - 65px - 48px)", // Subtract top-bar and tab-header height - overflowY: "auto", - // Style scrollbar - "&::-webkit-scrollbar": { - width: 1 - }, - "&::-webkit-scrollbar-track": { - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)" - }, - "&::-webkit-scrollbar-thumb": { - backgroundColor: "darkgrey", - outline: "1px solid slategrey" - } -}); - -const StyleSettingsTabContent = () => { - const [element] = useActiveElement(); - const elementStyleSettings = useElementStyleSettings(); - - if (!element) { - return ( - <NoActiveElement - icon={<TouchIcon />} - message={"Select an element on the canvas to activate this panel."} - /> - ); - } - - return ( - <RootElement> - {elementStyleSettings.length ? ( - elementStyleSettings.map(({ plugin, options }, index) => { - return React.cloneElement(plugin.render({ options }), { - key: index, - defaultAccordionValue: index === 0 - }); - }) - ) : ( - <NoActiveElement - icon={<WarningIcon />} - message={"No style settings found for selected element."} - /> - )} - </RootElement> - ); -}; - -export default React.memo(StyleSettingsTabContent); diff --git a/packages/app-page-builder/src/editor/components/Editor/Sidebar/icons/touch_app.svg b/packages/app-page-builder/src/editor/components/Editor/Sidebar/icons/touch_app.svg deleted file mode 100644 index 68978a0d45e..00000000000 --- a/packages/app-page-builder/src/editor/components/Editor/Sidebar/icons/touch_app.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M8.79,9.24V5.5c0-1.38,1.12-2.5,2.5-2.5s2.5,1.12,2.5,2.5v3.74c1.21-0.81,2-2.18,2-3.74c0-2.49-2.01-4.5-4.5-4.5 s-4.5,2.01-4.5,4.5C6.79,7.06,7.58,8.43,8.79,9.24z M14.29,11.71c-0.28-0.14-0.58-0.21-0.89-0.21h-0.61v-6 c0-0.83-0.67-1.5-1.5-1.5s-1.5,0.67-1.5,1.5v10.74l-3.44-0.72c-0.37-0.08-0.76,0.04-1.03,0.31c-0.43,0.44-0.43,1.14,0,1.58 l4.01,4.01C9.71,21.79,10.22,22,10.75,22h6.1c1,0,1.84-0.73,1.98-1.72l0.63-4.47c0.12-0.85-0.32-1.69-1.09-2.07L14.29,11.71z"/></g></g></svg> \ No newline at end of file diff --git a/packages/app-page-builder/src/editor/components/Editor/Sidebar/icons/warning-black.svg b/packages/app-page-builder/src/editor/components/Editor/Sidebar/icons/warning-black.svg deleted file mode 100644 index 03e668333f9..00000000000 --- a/packages/app-page-builder/src/editor/components/Editor/Sidebar/icons/warning-black.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="black" width="24px" height="24px"><path d="M4.47 21h15.06c1.54 0 2.5-1.67 1.73-3L13.73 4.99c-.77-1.33-2.69-1.33-3.46 0L2.74 18c-.77 1.33.19 3 1.73 3zM12 14c-.55 0-1-.45-1-1v-2c0-.55.45-1 1-1s1 .45 1 1v2c0 .55-.45 1-1 1zm1 4h-2v-2h2v2z"/></svg> \ No newline at end of file diff --git a/packages/app-page-builder/src/editor/components/Editor/Toolbar.tsx b/packages/app-page-builder/src/editor/components/Editor/Toolbar.tsx deleted file mode 100644 index 35026ac541c..00000000000 --- a/packages/app-page-builder/src/editor/components/Editor/Toolbar.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import React, { ReactElement, useEffect, useRef } from "react"; -import { useRecoilValue } from "recoil"; -import classNames from "classnames"; -import styled from "@emotion/styled"; -import { css } from "emotion"; -import { DrawerLeft, DrawerContent } from "@webiny/ui/Drawer"; -import { plugins } from "@webiny/plugins"; -import { makeDecoratable } from "@webiny/app-admin"; -import { PbEditorToolbarBottomPlugin, PbEditorToolbarTopPlugin } from "~/types"; -import { useKeyHandler } from "~/editor/hooks/useKeyHandler"; -import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; -import { DeactivatePluginActionEvent } from "~/editor/recoil/actions"; -import { activePluginsByTypeNamesSelector } from "~/editor/recoil/modules"; - -const ToolbarDrawerContainer = styled("div")({ - top: 64, - left: 0, - position: "fixed", - backgroundColor: "var(--mdc-theme-surface)", - zIndex: 2 -}); -const ToolbarContainer = styled("div")({ - position: "fixed", - display: "inline-block", - padding: "2px 1px 2px 3px", - width: 50, - top: 64, - height: "calc(100vh - 68px)", - backgroundColor: "var(--mdc-theme-surface)", - boxShadow: "1px 0px 5px 0px var(--mdc-theme-on-background)", - zIndex: 3 -}); -const DrawerContainer = styled("div")<{ open: boolean }>(({ open }) => ({ - pointerEvents: open ? "all" : "none", - ".mdc-drawer__drawer": { - "> .mdc-list": { - padding: 0 - } - } -})); -const ToolbarActionsWrapper = styled("div")({ - position: "relative", - display: "flex", - flexDirection: "column", - justifyContent: "space-between", - height: "100%" -}); -const drawerStyle = css({ - zIndex: 10, - "&.mdc-drawer--dismissible": { - marginLeft: 54, - position: "fixed", - top: 64, - height: "calc(100vh - 64px)", - width: "280px !important", - maxWidth: "280px !important", - ".mdc-drawer__content": { - width: "100%" - } - } -}); -interface ToolbarDrawerProps { - name: string; - active: boolean; - children: React.ReactNode; - drawerClassName?: string; -} -const ToolbarDrawer = ({ name, active, children, drawerClassName }: ToolbarDrawerProps) => { - const eventActionHandler = useEventActionHandler(); - const { removeKeyHandler, addKeyHandler } = useKeyHandler(); - const last = useRef<{ active: boolean | null }>({ - active: null - }); - useEffect(() => { - if (active && !last.current.active) { - addKeyHandler("escape", e => { - e.preventDefault(); - eventActionHandler.trigger( - new DeactivatePluginActionEvent({ - name - }) - ); - }); - } - if (!active && last.current.active) { - removeKeyHandler("escape"); - } - }); - useEffect(() => { - last.current = { - active - }; - }); - return ( - <DrawerContainer open={active}> - <DrawerLeft - dismissible - open={active} - className={classNames(drawerStyle, drawerClassName)} - > - <DrawerContent>{children}</DrawerContent> - </DrawerLeft> - </DrawerContainer> - ); -}; -export const renderPlugin = (plugin: PbEditorToolbarTopPlugin | PbEditorToolbarBottomPlugin) => { - return React.cloneElement(plugin.renderAction(), { key: plugin.name }); -}; - -const Toolbar = () => { - const activePluginsTop = useRecoilValue( - activePluginsByTypeNamesSelector("pb-editor-toolbar-top") - ); - const actionsTop = plugins.byType<PbEditorToolbarTopPlugin>("pb-editor-toolbar-top"); - const actionsBottom = plugins.byType<PbEditorToolbarBottomPlugin>("pb-editor-toolbar-bottom"); - return ( - <> - <ToolbarDrawerContainer> - {actionsTop - .filter(plugin => typeof plugin.renderDrawer === "function") - .map(plugin => ( - <ToolbarDrawer - key={plugin.name} - name={plugin.name as string} - active={Boolean(activePluginsTop.includes(plugin.name as string))} - drawerClassName={plugin.toolbar && plugin.toolbar.drawerClassName} - > - {(plugin.renderDrawer as () => ReactElement)()} - </ToolbarDrawer> - ))} - </ToolbarDrawerContainer> - <ToolbarContainer> - <ToolbarActions> - <div>{actionsTop.map(renderPlugin)}</div> - <div>{actionsBottom.map(renderPlugin)}</div> - </ToolbarActions> - </ToolbarContainer> - </> - ); -}; -export default React.memo(Toolbar); - -export interface ToolbarActionsProps { - children: React.ReactNode; -} - -export const ToolbarActions = makeDecoratable( - "ToolbarActions", - ({ children }: ToolbarActionsProps) => { - return <ToolbarActionsWrapper>{children}</ToolbarActionsWrapper>; - } -); diff --git a/packages/app-page-builder/src/editor/config/Background/Background.tsx b/packages/app-page-builder/src/editor/config/Background/Background.tsx deleted file mode 100644 index ccf1cc607c8..00000000000 --- a/packages/app-page-builder/src/editor/config/Background/Background.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { useCallback } from "react"; -import { createDecorator } from "@webiny/app-admin"; -import { css } from "emotion"; -import { EditorContent } from "~/editor"; -import { useActiveElement } from "~/editor/hooks/useActiveElement"; - -const backgroundStyle = css({ - position: "fixed", - top: 0, - left: 0, - width: "100%", - minHeight: "100%" -}); - -const Background = () => { - const [activeElement, setActiveElement] = useActiveElement(); - - const deactivateElement = useCallback(() => { - if (!activeElement) { - return; - } - setActiveElement(null); - }, [activeElement]); - - return <div className={backgroundStyle} onClick={deactivateElement} />; -}; - -export const BackgroundPlugin = createDecorator(EditorContent, PrevContent => { - return function AddBackground() { - return ( - <> - <PrevContent /> - <Background /> - </> - ); - }; -}); diff --git a/packages/app-page-builder/src/editor/config/Content/Content.tsx b/packages/app-page-builder/src/editor/config/Content/Content.tsx new file mode 100644 index 00000000000..d46a9dd818d --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Content/Content.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { makeDecoratable } from "@webiny/app-admin"; +import { Layout } from "./Layout"; +import { Elements as BaseElements } from "~/editor/config/Elements"; +import { Element as BaseElement, ElementProps as BaseElementProps } from "~/editor/config/Element"; +import { createGetId } from "~/editor/config/createGetId"; + +const SCOPE = "content"; + +const BaseContent = makeDecoratable("EditorContent", () => { + return ( + <Layout> + <Elements /> + </Layout> + ); +}); + +export type ElementProps = Omit<BaseElementProps, "scope" | "group" | "id">; + +const getElementId = createGetId(SCOPE)(); + +const Element = makeDecoratable("ContentElement", (props: ElementProps) => { + return ( + <BaseElement + {...props} + scope={SCOPE} + id={getElementId(props.name)} + before={props.before ? getElementId(props.before) : undefined} + after={props.after ? getElementId(props.after) : undefined} + /> + ); +}); + +const Elements = makeDecoratable("ContentElements", () => { + return <BaseElements scope={SCOPE} />; +}); + +export const Content = Object.assign(BaseContent, { Layout, Element, Elements }); diff --git a/packages/app-page-builder/src/editor/config/Content/Layout.tsx b/packages/app-page-builder/src/editor/config/Content/Layout.tsx new file mode 100644 index 00000000000..99d29fac1ba --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Content/Layout.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { css } from "emotion"; +import { Elevation } from "@webiny/ui/Elevation"; + +export interface LayoutProps { + className?: string; + children: React.ReactNode; +} + +const contentContainerWrapper = css` + margin: 95px 65px 50px 85px; + padding: 0; + position: absolute; + width: calc(100vw - 415px); + top: 0; + box-sizing: border-box; + z-index: 1; +`; + +export const Layout = ({ className = contentContainerWrapper, children }: LayoutProps) => { + return ( + <Elevation className={className} z={0}> + {children} + </Elevation> + ); +}; diff --git a/packages/app-page-builder/src/editor/config/EditorConfig.tsx b/packages/app-page-builder/src/editor/config/EditorConfig.tsx new file mode 100644 index 00000000000..3ebc9182ff6 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/EditorConfig.tsx @@ -0,0 +1,33 @@ +import { createConfigurableComponent } from "@webiny/react-properties"; +import { Element, ElementConfig } from "./Element"; +import { TopBar } from "./TopBar/TopBar"; +import { Layout } from "./Layout"; +import { Content } from "./Content/Content"; +import { Toolbar } from "./Toolbar/Toolbar"; +import { Sidebar } from "./Sidebar/Sidebar"; +import { Elements } from "~/editor/config/Elements"; + +const base = createConfigurableComponent<ContentEntryEditorConfig>("PageBuilderEditorConfig"); + +export const EditorConfig = Object.assign(base.Config, { + Element, + Elements, + Layout, + Content, + TopBar, + Toolbar, + Sidebar, + useEditorConfig +}); + +export const EditorWithConfig = Object.assign(base.WithConfig, { displayName: "EditorWithConfig" }); + +interface ContentEntryEditorConfig { + elements: ElementConfig[]; +} + +export function useEditorConfig() { + const config = base.useConfig(); + + return { elements: config.elements || [] }; +} diff --git a/packages/app-page-builder/src/editor/config/EditorDefaultConfig.tsx b/packages/app-page-builder/src/editor/config/EditorDefaultConfig.tsx deleted file mode 100644 index c50bb4f2dcc..00000000000 --- a/packages/app-page-builder/src/editor/config/EditorDefaultConfig.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import { ResponsiveModeSelectorPlugin } from "./ResponsiveModeSelector"; -import { BreadcrumbsPlugin } from "./Breadcrumbs"; -import { BackgroundPlugin } from "./Background"; -import { ActionPlugins } from "./ActionPlugins"; - -export const EditorDefaultConfig = () => { - return ( - <> - <ActionPlugins /> - <ResponsiveModeSelectorPlugin /> - <BreadcrumbsPlugin /> - <BackgroundPlugin /> - </> - ); -}; diff --git a/packages/app-page-builder/src/editor/config/Element.tsx b/packages/app-page-builder/src/editor/config/Element.tsx new file mode 100644 index 00000000000..35df70f9c1b --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Element.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { makeDecoratable } from "@webiny/app-admin"; +import { Property, useIdGenerator } from "@webiny/react-properties"; + +export interface ElementConfig { + name: string; + group: string; + scope: string; + element: JSX.Element; +} + +export interface ElementProps { + name: string; + id?: string; + element?: JSX.Element | null; + group?: string; + scope?: string; + remove?: boolean; + before?: string; + after?: string; +} + +export const Element = makeDecoratable( + "EditorElement", + ({ id, name, element, group, scope, remove, before, after }: ElementProps) => { + const getId = useIdGenerator("element"); + const realId = id ?? name; + + const placeAfter = after !== undefined ? getId(after) : undefined; + const placeBefore = before !== undefined ? getId(before) : undefined; + + return ( + <Property + id={getId(realId)} + name={"elements"} + remove={remove} + array={true} + before={placeBefore} + after={placeAfter} + > + <Property id={getId(realId, "name")} name={"name"} value={name} /> + {element ? ( + <Property id={getId(realId, "element")} name={"element"} value={element} /> + ) : null} + {group ? ( + <Property id={getId(realId, "group")} name={"group"} value={group} /> + ) : null} + {scope ? ( + <Property id={getId(realId, "scope")} name={"scope"} value={scope} /> + ) : null} + </Property> + ); + } +); diff --git a/packages/app-page-builder/src/editor/config/Elements.tsx b/packages/app-page-builder/src/editor/config/Elements.tsx new file mode 100644 index 00000000000..44df2e98884 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Elements.tsx @@ -0,0 +1,45 @@ +import React, { useMemo } from "react"; +import { useEditorConfig } from "./EditorConfig"; +import { ElementConfig } from "~/editor/config/Element"; + +declare global { + // eslint-disable-next-line + namespace JSX { + interface IntrinsicElements { + "pb-editor-elements": React.HTMLProps<HTMLDivElement>; + "pb-editor-element": React.HTMLProps<HTMLDivElement>; + } + } +} + +export interface ElementsProps { + group?: string; + scope?: string; +} + +const passthrough = () => true; + +const byGroup = (group?: string) => { + return group ? (item: ElementConfig) => item.group === group : passthrough; +}; +const byScope = (scope?: string) => { + return scope ? (item: ElementConfig) => item.scope === scope : passthrough; +}; + +export const Elements = ({ group, scope }: ElementsProps) => { + const { elements } = useEditorConfig(); + + const groupElements = useMemo(() => { + return elements.filter(byGroup(group)).filter(byScope(scope)); + }, [elements, group, scope]); + + return ( + <pb-editor-elements data-scope={scope} data-group={group}> + {groupElements.map(element => ( + <pb-editor-element key={element.name} data-name={element.name}> + {element.element} + </pb-editor-element> + ))} + </pb-editor-elements> + ); +}; diff --git a/packages/app-page-builder/src/editor/config/Layout.tsx b/packages/app-page-builder/src/editor/config/Layout.tsx new file mode 100644 index 00000000000..7a478e71fb8 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Layout.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { makeDecoratable } from "@webiny/react-composition"; +import { EditorConfig } from "~/editor/config/EditorConfig"; + +export const Layout = makeDecoratable("EditorLayout", () => { + return ( + <> + <EditorConfig.TopBar /> + <EditorConfig.Toolbar /> + <EditorConfig.Content /> + <EditorConfig.Sidebar /> + <EditorConfig.Elements group={"overlays"} /> + </> + ); +}); diff --git a/packages/app-page-builder/src/editor/config/ResponsiveModeSelector/ResponsiveModeSelectorPlugin.tsx b/packages/app-page-builder/src/editor/config/ResponsiveModeSelector/ResponsiveModeSelectorPlugin.tsx deleted file mode 100644 index 035963f6064..00000000000 --- a/packages/app-page-builder/src/editor/config/ResponsiveModeSelector/ResponsiveModeSelectorPlugin.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; -import { createDecorator } from "@webiny/app-admin"; -import { EditorBar } from "~/editor"; -import { ResponsiveModeSelector } from "./ResponsiveModeSelector"; - -export const ResponsiveModeSelectorPlugin = createDecorator( - EditorBar.CenterSection, - CenterSection => { - return function AddResponsiveModeSelector(props) { - return ( - <CenterSection> - {props.children} - <ResponsiveModeSelector /> - </CenterSection> - ); - }; - } -); diff --git a/packages/app-page-builder/src/editor/config/ResponsiveModeSelector/index.ts b/packages/app-page-builder/src/editor/config/ResponsiveModeSelector/index.ts deleted file mode 100644 index 0547fc71e3a..00000000000 --- a/packages/app-page-builder/src/editor/config/ResponsiveModeSelector/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ResponsiveModeSelectorPlugin } from "./ResponsiveModeSelectorPlugin"; diff --git a/packages/app-page-builder/src/editor/config/Sidebar/IconButton.tsx b/packages/app-page-builder/src/editor/config/Sidebar/IconButton.tsx new file mode 100644 index 00000000000..77b322acb60 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Sidebar/IconButton.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { IconButton as BaseIconButton } from "@webiny/ui/Button"; +import { Tooltip } from "@webiny/ui/Tooltip"; + +export interface IconButtonProps { + icon: JSX.Element; + label: string; + disabled?: boolean; + onClick?: () => void; +} + +export const IconButton = ({ label, icon, disabled = false, onClick }: IconButtonProps) => { + return ( + <Tooltip placement={"bottom"} content={label}> + <BaseIconButton icon={icon} onClick={onClick} disabled={disabled} /> + </Tooltip> + ); +}; diff --git a/packages/app-page-builder/src/editor/config/Sidebar/Layout.tsx b/packages/app-page-builder/src/editor/config/Sidebar/Layout.tsx new file mode 100644 index 00000000000..87ac87444d7 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Sidebar/Layout.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { css } from "emotion"; +import { makeDecoratable } from "@webiny/app-admin"; +import { Elevation } from "@webiny/ui/Elevation"; +import { Tabs } from "@webiny/ui/Tabs"; +import { Sidebar } from "./Sidebar"; +import { SidebarHighlight } from "./SidebarHighlight"; + +const rightSideBar = css({ + boxShadow: "1px 0px 5px 0px rgba(128,128,128,1)", + position: "fixed", + right: 0, + top: 65, + height: "100%", + width: 300, + zIndex: 1 +}); + +export interface LayoutProps { + className?: string; +} + +export const Layout = makeDecoratable( + "SidebarLayout", + ({ className = rightSideBar }: LayoutProps) => { + const { activeGroup, setActiveGroup } = Sidebar.useActiveGroup(); + + return ( + <Elevation z={1} className={className}> + <Tabs value={activeGroup} onActivate={setActiveGroup}> + <Sidebar.Elements group="groups" /> + </Tabs> + <SidebarHighlight /> + </Elevation> + ); + } +); diff --git a/packages/app-page-builder/src/editor/config/Sidebar/Sidebar.tsx b/packages/app-page-builder/src/editor/config/Sidebar/Sidebar.tsx new file mode 100644 index 00000000000..2c4b36b094e --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Sidebar/Sidebar.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { makeDecoratable } from "@webiny/app-admin"; +import { Element as CoreElement, ElementProps as CoreElementProps } from "../Element"; +import { Layout } from "./Layout"; +import { + Elements as BaseElements, + ElementsProps as BaseElementsProps +} from "~/editor/config/Elements"; +import { Tab } from "./Tab"; +import { useActiveGroup } from "~/editor/config/Sidebar/useActiveGroup"; +import { IconButton } from "./IconButton"; +import { createGetId } from "~/editor/config/createGetId"; + +const SCOPE = "sidebar"; + +const BaseSidebar = () => { + return <Layout />; +}; + +export type ScopedElementProps = Omit<CoreElementProps, "scope">; + +const ScopedElement = makeDecoratable("SidebarScopedElement", (props: ScopedElementProps) => { + return <CoreElement {...props} scope={SCOPE} />; +}); + +const getElementId = createGetId(SCOPE)(); + +export type BaseElementProps = Omit<ScopedElementProps, "id">; + +const BaseElement = makeDecoratable("SidebarElement", (props: BaseElementProps) => { + return ( + <ScopedElement + {...props} + id={getElementId(props.name)} + before={props.before ? getElementId(props.before) : undefined} + after={props.after ? getElementId(props.after) : undefined} + /> + ); +}); + +export type ElementsProps = Omit<BaseElementsProps, "scope">; + +const Elements = makeDecoratable("SidebarElements", (props: ElementsProps) => { + return <BaseElements {...props} scope={SCOPE} />; +}); + +export type ElementPropertyProps = Omit<BaseElementProps, "scope">; + +const getElementPropertyId = createGetId(SCOPE)("elementProperty"); + +const BaseElementProperty = makeDecoratable( + "SidebarElementProperty", + (props: ElementPropertyProps) => { + return ( + <ScopedElement + {...props} + id={getElementPropertyId(props.name)} + before={props.before ? getElementPropertyId(props.before) : undefined} + after={props.after ? getElementPropertyId(props.after) : undefined} + /> + ); + } +); + +const ElementPropertyGroups = { + STYLE_GROUP: "styleProperties", + ELEMENT_GROUP: "elementProperties" +}; + +export type GroupProps = Omit<BaseElementProps, "group">; + +const BaseGroup = makeDecoratable("SidebarGroup", (props: GroupProps) => { + return ( + <ScopedElement + {...props} + group={"groups"} + id={getElementId(props.name)} + before={props.before ? getElementId(props.before) : undefined} + after={props.after ? getElementId(props.after) : undefined} + /> + ); +}); + +const getElementActionId = createGetId(SCOPE)("elementAction"); + +export type ElementActionProps = Omit<BaseElementProps, "group">; + +const BaseElementAction = makeDecoratable("SidebarElementAction", (props: ElementActionProps) => { + return ( + <ScopedElement + {...props} + group={"actions"} + id={getElementActionId(props.name)} + before={props.before ? getElementActionId(props.before) : undefined} + after={props.after ? getElementActionId(props.after) : undefined} + /> + ); +}); + +export const Sidebar = Object.assign(BaseSidebar, { + Layout, + Element: BaseElement, + ElementProperty: Object.assign(BaseElementProperty, ElementPropertyGroups), + Elements, + Group: Object.assign(BaseGroup, { Tab }), + ElementAction: Object.assign(BaseElementAction, { + IconButton + }), + useActiveGroup +}); diff --git a/packages/app-page-builder/src/editor/config/Sidebar/SidebarHighlight.tsx b/packages/app-page-builder/src/editor/config/Sidebar/SidebarHighlight.tsx new file mode 100644 index 00000000000..8bd66050fc9 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Sidebar/SidebarHighlight.tsx @@ -0,0 +1,39 @@ +import React, { useCallback, useEffect } from "react"; +import styled from "@emotion/styled"; +import { highlightSidebarTabMutation } from "~/editor/recoil/modules"; +import { useElementSidebar } from "~/editor/hooks/useElementSidebar"; + +const PanelHighLight = styled("div")({ + "&": { + opacity: 0, + animation: "wf-blink-in 1s", + border: "2px solid var(--mdc-theme-secondary)", + boxShadow: "0 0 15px var(--mdc-theme-secondary)", + backgroundColor: "rgba(42, 217, 134, 0.25)", + borderRadius: "2px", + position: "absolute", + top: "0", + left: "0", + right: "0", + bottom: "0", + zIndex: 1, + pointerEvents: "none" + }, + "@keyframes wf-blink-in": { "40%": { opacity: 1 } } +}); + +export const SidebarHighlight = () => { + const [sidebar, setSidebar] = useElementSidebar(); + + const unhighlightElementTab = useCallback(() => { + setSidebar(prev => highlightSidebarTabMutation(prev, false)); + }, []); + + useEffect(() => { + if (sidebar.highlightTab) { + setTimeout(unhighlightElementTab, 1000); + } + }, [sidebar.highlightTab]); + + return sidebar.highlightTab ? <PanelHighLight /> : null; +}; diff --git a/packages/app-page-builder/src/editor/config/Sidebar/Tab.tsx b/packages/app-page-builder/src/editor/config/Sidebar/Tab.tsx new file mode 100644 index 00000000000..5e09390ffb2 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Sidebar/Tab.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import styled from "@emotion/styled"; +import { Tab as UiTab } from "@webiny/ui/Tabs"; + +export const TabContainer = styled("div")({ + display: "flex", + flexDirection: "column", + height: "calc(100vh - 65px - 48px)", // Subtract top-bar and tab-header height + overflowY: "auto", + // Style scrollbar + "&::-webkit-scrollbar": { + width: 1 + }, + "&::-webkit-scrollbar-track": { + boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)" + }, + "&::-webkit-scrollbar-thumb": { + backgroundColor: "darkgrey", + outline: "1px solid slategrey" + } +}); + +export interface TabProps { + label: string; + element: JSX.Element; + disabled?: boolean; + visible?: boolean; +} + +export const Tab = ({ label, disabled, element, visible }: TabProps) => { + return ( + <UiTab label={label} disabled={disabled} visible={visible}> + <TabContainer>{element}</TabContainer> + </UiTab> + ); +}; diff --git a/packages/app-page-builder/src/editor/config/Sidebar/useActiveGroup.ts b/packages/app-page-builder/src/editor/config/Sidebar/useActiveGroup.ts new file mode 100644 index 00000000000..98a649c8e34 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Sidebar/useActiveGroup.ts @@ -0,0 +1,29 @@ +import { useCallback } from "react"; +import store from "store"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; +import { useElementSidebar } from "~/editor/hooks/useElementSidebar"; +import { updateSidebarActiveTabIndexMutation } from "~/editor/recoil/modules"; + +const LOCAL_STORAGE_KEY = "webiny_pb_editor_active_tab"; + +export function useActiveGroup() { + const [element] = useActiveElement(); + const [sidebar, setSidebar] = useElementSidebar(); + + const activeGroup = store.get(LOCAL_STORAGE_KEY, sidebar.activeTabIndex) ?? 0; + + const setActiveGroup = useCallback( + index => { + setSidebar(prev => updateSidebarActiveTabIndexMutation(prev, index)); + if (element) { + store.set(LOCAL_STORAGE_KEY, index); + } + }, + [element] + ); + + return { + activeGroup, + setActiveGroup + }; +} diff --git a/packages/app-page-builder/src/editor/config/Toolbar/Drawer.tsx b/packages/app-page-builder/src/editor/config/Toolbar/Drawer.tsx new file mode 100644 index 00000000000..88f19838251 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Toolbar/Drawer.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { css } from "emotion"; +import styled from "@emotion/styled"; +import classNames from "classnames"; +import { DrawerContent, DrawerLeft } from "@webiny/ui/Drawer"; +import { useDrawer } from "./DrawerProvider"; + +const DrawerContainer = styled("div")<{ open: boolean }>(({ open }) => ({ + pointerEvents: open ? "all" : "none", + ".mdc-drawer__drawer": { + "> .mdc-list": { + padding: 0 + } + } +})); + +const drawerStyle = css({ + zIndex: 10, + "&.mdc-drawer--dismissible": { + marginLeft: 54, + position: "fixed", + top: 64, + height: "calc(100vh - 64px)", + width: "280px !important", + maxWidth: "280px !important", + ".mdc-drawer__content": { + width: "100%" + } + } +}); + +export interface DrawerProps { + children: React.ReactNode; + drawerClassName?: string; +} + +export const Drawer = ({ children, drawerClassName }: DrawerProps) => { + const { isOpen } = useDrawer(); + + return ( + <DrawerContainer open={isOpen}> + <DrawerLeft + dismissible + open={isOpen} + className={classNames(drawerStyle, drawerClassName)} + > + <DrawerContent>{children}</DrawerContent> + </DrawerLeft> + </DrawerContainer> + ); +}; diff --git a/packages/app-page-builder/src/editor/config/Toolbar/DrawerProvider.tsx b/packages/app-page-builder/src/editor/config/Toolbar/DrawerProvider.tsx new file mode 100644 index 00000000000..22236a32767 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Toolbar/DrawerProvider.tsx @@ -0,0 +1,38 @@ +import React, { useContext } from "react"; +import { Drawer, useDrawers } from "./DrawersProvider"; + +export interface DrawerContext { + isOpen: boolean; + close: () => void; + open: () => void; +} + +const DrawerContext = React.createContext<DrawerContext | undefined>(undefined); + +interface DrawerProviderProps { + drawer: Drawer; + children: React.ReactNode; +} + +export const DrawerProvider = ({ drawer, children }: DrawerProviderProps) => { + const { isActive, setActive } = useDrawers(); + + const context: DrawerContext = { + isOpen: isActive(drawer.id), + open: () => setActive(drawer.id), + close: () => setActive(undefined) + }; + + return <DrawerContext.Provider value={context}>{children}</DrawerContext.Provider>; +}; + +export function useDrawer() { + const context = useContext(DrawerContext); + if (!context) { + throw new Error( + `Missing DrawerProvider in the component hierarchy! Are you using the "useDrawer()" hook in the right place?` + ); + } + + return context; +} diff --git a/packages/app-page-builder/src/editor/config/Toolbar/DrawerTrigger.tsx b/packages/app-page-builder/src/editor/config/Toolbar/DrawerTrigger.tsx new file mode 100644 index 00000000000..605b694fae4 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Toolbar/DrawerTrigger.tsx @@ -0,0 +1,31 @@ +import React, { useEffect, useRef } from "react"; +import { generateId } from "@webiny/utils"; +import { useDrawers } from "./DrawersProvider"; +import { IconButton } from "./IconButton"; + +export interface DrawerTriggerProps { + icon: JSX.Element; + label: string; + drawer: JSX.Element; +} + +export const DrawerTrigger = ({ drawer, label, icon }: DrawerTriggerProps) => { + const drawerId = useRef(generateId()); + const { isActive, setActive, registerDrawer } = useDrawers(); + + useEffect(() => { + return registerDrawer(drawerId.current, drawer); + }, []); + + const toggleDrawer = () => { + const id = drawerId.current; + if (isActive(id)) { + setActive(undefined); + return; + } + + setActive(id); + }; + + return <IconButton icon={icon} label={label} onClick={toggleDrawer} />; +}; diff --git a/packages/app-page-builder/src/editor/config/Toolbar/DrawersProvider.tsx b/packages/app-page-builder/src/editor/config/Toolbar/DrawersProvider.tsx new file mode 100644 index 00000000000..eb595d17b79 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Toolbar/DrawersProvider.tsx @@ -0,0 +1,67 @@ +import React, { useContext, useEffect, useState } from "react"; +import { useKeyHandler } from "~/editor/hooks/useKeyHandler"; + +export interface Drawer { + id: string; + element: JSX.Element; +} + +export type Drawers = Drawer[]; + +export interface DrawerContext { + drawers: Drawers; + isActive: (id: string) => boolean; + setActive: (id: string | undefined) => void; + registerDrawer: (id: string, drawer: JSX.Element) => () => void; +} + +const DrawerContext = React.createContext<DrawerContext | undefined>(undefined); + +export interface DrawerProviderProps { + children: React.ReactNode; +} + +export const DrawersProvider = ({ children }: DrawerProviderProps) => { + const [activeId, setActive] = useState<string | undefined>(undefined); + const [drawers, setDrawers] = useState<Drawers>([]); + + const { removeKeyHandler, addKeyHandler } = useKeyHandler(); + + useEffect(() => { + addKeyHandler("escape", e => { + e.preventDefault(); + setActive(undefined); + }); + + return () => { + removeKeyHandler("escape"); + }; + }); + + const context: DrawerContext = { + drawers, + isActive: id => activeId === id, + setActive: id => setActive(id), + registerDrawer: (id, element) => { + setDrawers(drawers => [...drawers, { id, element }]); + + return () => { + setDrawers(drawers => { + return drawers.filter(drawer => drawer.id !== id); + }); + }; + } + }; + return <DrawerContext.Provider value={context}>{children}</DrawerContext.Provider>; +}; + +export function useDrawers() { + const context = useContext(DrawerContext); + if (!context) { + throw new Error( + `Missing DrawersProvider in the component hierarchy! Are you using the "useDrawers()" hook in the right place?` + ); + } + + return context; +} diff --git a/packages/app-page-builder/src/editor/config/Toolbar/IconButton.tsx b/packages/app-page-builder/src/editor/config/Toolbar/IconButton.tsx new file mode 100644 index 00000000000..2b9114cd71a --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Toolbar/IconButton.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { IconButton as BaseIconButton } from "@webiny/ui/Button"; +import { Tooltip } from "@webiny/ui/Tooltip"; + +export interface IconButtonProps { + icon: JSX.Element; + label: string; + onClick?: () => void; +} + +export const IconButton = ({ label, icon, onClick }: IconButtonProps) => { + return ( + <Tooltip placement={"right"} content={label}> + <BaseIconButton icon={icon} onClick={onClick} /> + </Tooltip> + ); +}; diff --git a/packages/app-page-builder/src/editor/config/Toolbar/Layout.tsx b/packages/app-page-builder/src/editor/config/Toolbar/Layout.tsx new file mode 100644 index 00000000000..d63f870ca2e --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Toolbar/Layout.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import styled from "@emotion/styled"; +import { Toolbar } from "./Toolbar"; +import { useDrawers } from "./DrawersProvider"; +import { DrawerProvider } from "./DrawerProvider"; + +const ToolbarDrawerContainer = styled("div")({ + top: 64, + left: 0, + position: "fixed", + backgroundColor: "var(--mdc-theme-surface)", + zIndex: 2 +}); +const ToolbarContainer = styled("div")({ + position: "fixed", + display: "inline-block", + padding: "2px 1px 2px 3px", + width: 50, + top: 64, + height: "calc(100vh - 68px)", + backgroundColor: "var(--mdc-theme-surface)", + boxShadow: "1px 0px 5px 0px var(--mdc-theme-on-background)", + zIndex: 3 +}); + +const ToolbarActions = styled("div")({ + position: "relative", + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + height: "100%" +}); + +export const Layout = () => { + const { drawers } = useDrawers(); + + return ( + <> + <ToolbarDrawerContainer> + {drawers.map(drawer => ( + <DrawerProvider key={drawer.id} drawer={drawer}> + {drawer.element} + </DrawerProvider> + ))} + </ToolbarDrawerContainer> + <ToolbarContainer> + <ToolbarActions> + <div> + <Toolbar.Elements group={"top"} /> + </div> + <div> + <Toolbar.Elements group={"bottom"} /> + </div> + </ToolbarActions> + </ToolbarContainer> + </> + ); +}; diff --git a/packages/app-page-builder/src/editor/config/Toolbar/Toolbar.tsx b/packages/app-page-builder/src/editor/config/Toolbar/Toolbar.tsx new file mode 100644 index 00000000000..0f3617076db --- /dev/null +++ b/packages/app-page-builder/src/editor/config/Toolbar/Toolbar.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { makeDecoratable } from "@webiny/app-admin"; +import { Element as BaseElement, ElementProps as BaseElementProps } from "../Element"; +import { Layout } from "./Layout"; +import { + Elements as BaseElements, + ElementsProps as BaseElementsProps +} from "~/editor/config/Elements"; +import { DrawersProvider } from "./DrawersProvider"; +import { DrawerTrigger } from "./DrawerTrigger"; +import { Drawer } from "./Drawer"; +import { IconButton } from "./IconButton"; +import { createGetId } from "~/editor/config/createGetId"; + +const SCOPE = "toolbar"; + +const BaseToolbar = () => { + return ( + <DrawersProvider> + <Layout /> + </DrawersProvider> + ); +}; + +const getElementId = createGetId(SCOPE)(); + +export type ElementProps = Omit<BaseElementProps, "scope" | "id">; + +const BaseToolbarElement = makeDecoratable("ToolbarElement", (props: ElementProps) => { + return ( + <BaseElement + {...props} + scope={SCOPE} + id={getElementId(props.name)} + before={props.before ? getElementId(props.before) : undefined} + after={props.after ? getElementId(props.after) : undefined} + /> + ); +}); + +export type ElementsProps = Omit<BaseElementsProps, "scope">; + +const Elements = makeDecoratable("ToolbarElements", (props: ElementsProps) => { + return <BaseElements {...props} scope={SCOPE} />; +}); + +export const Toolbar = Object.assign(BaseToolbar, { + Layout, + Element: Object.assign(BaseToolbarElement, { DrawerTrigger, Drawer, IconButton }), + Elements +}); diff --git a/packages/app-page-builder/src/editor/config/TopBar/Divider.tsx b/packages/app-page-builder/src/editor/config/TopBar/Divider.tsx new file mode 100644 index 00000000000..10f336ec6a2 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/TopBar/Divider.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import styled from "@emotion/styled"; +import { makeDecoratable } from "@webiny/app-admin"; + +const StyledDivider = styled.div` + width: 1px; + margin: 0 5px; + height: 100%; + background-color: var(--mdc-theme-on-background); +`; + +export const Divider = makeDecoratable("TopBarDivider", () => { + return <StyledDivider />; +}); diff --git a/packages/app-page-builder/src/editor/config/TopBar/Layout.tsx b/packages/app-page-builder/src/editor/config/TopBar/Layout.tsx new file mode 100644 index 00000000000..8b4b509c616 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/TopBar/Layout.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { css } from "emotion"; +import { makeDecoratable } from "@webiny/app-admin"; +import { TopAppBar, TopAppBarSection } from "@webiny/ui/TopAppBar"; +import { EditorConfig } from "~/editor/config"; + +const topBar = css` + box-shadow: 1px 0px 5px 0px rgba(128, 128, 128, 1); + height: 64px; + display: flex; + > * { + flex: 1; + } +`; + +const centerTopBar = css` + &.mdc-top-app-bar__section { + padding-top: 0; + padding-bottom: 0; + } +`; + +const oneThird = { width: "33%" }; + +export interface TopBarLayoutProps { + fixed?: boolean; + className?: string; +} + +export const Layout = makeDecoratable("TopBarLayout", (props: TopBarLayoutProps) => { + return ( + <TopAppBar className={props.className ?? topBar} fixed={props.fixed ?? true}> + <TopAppBarSection alignStart style={oneThird}> + <EditorConfig.TopBar.Elements group={"left"} /> + </TopAppBarSection> + <TopAppBarSection className={centerTopBar} style={oneThird}> + <EditorConfig.TopBar.Elements group={"center"} /> + </TopAppBarSection> + <TopAppBarSection alignEnd style={oneThird}> + <EditorConfig.TopBar.Elements group={"actions"} /> + </TopAppBarSection> + </TopAppBar> + ); +}); diff --git a/packages/app-page-builder/src/editor/config/TopBar/MenuItem.tsx b/packages/app-page-builder/src/editor/config/TopBar/MenuItem.tsx new file mode 100644 index 00000000000..4290944e1d9 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/TopBar/MenuItem.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { ListItemGraphic } from "@webiny/ui/List"; +import { Icon } from "@webiny/ui/Icon"; +import { MenuItem as UiMenuItem } from "@webiny/ui/Menu"; + +export interface MenuItemProps { + label: string; + onClick: () => void; + icon: JSX.Element; + "data-testid"?: string; + disabled?: boolean; +} + +export const MenuItem = (props: MenuItemProps) => { + return ( + <UiMenuItem + onClick={props.onClick} + disabled={Boolean(props.disabled)} + data-testid={props["data-testid"]} + > + <ListItemGraphic> + <Icon icon={props.icon} /> + </ListItemGraphic> + {props.label} + </UiMenuItem> + ); +}; diff --git a/packages/app-page-builder/src/editor/config/TopBar/TopBar.tsx b/packages/app-page-builder/src/editor/config/TopBar/TopBar.tsx new file mode 100644 index 00000000000..4cafd48f067 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/TopBar/TopBar.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { makeDecoratable } from "@webiny/app-admin"; +import { Element as BaseElement, ElementProps as BaseElementProps } from "../Element"; +import { Elements as BaseElements, ElementsProps as BaseElementsProps } from "../Elements"; +import { Divider } from "./Divider"; +import { Layout } from "./Layout"; +import { createGetId } from "~/editor/config/createGetId"; +import { MenuItem } from "./MenuItem"; + +const SCOPE = "topBar"; + +const BaseTopBar = () => { + return <Layout />; +}; + +const getElementId = createGetId(SCOPE)(); + +export type ElementProps = Omit<BaseElementProps, "scope" | "id">; + +const Element = makeDecoratable("TopBarElement", (props: ElementProps) => { + return ( + <BaseElement + {...props} + scope={SCOPE} + id={getElementId(props.name)} + before={props.before ? getElementId(props.before) : undefined} + after={props.after ? getElementId(props.after) : undefined} + /> + ); +}); + +export type ActionProps = Omit<ElementProps, "group">; + +const Action = makeDecoratable("TopBarAction", (props: ActionProps) => { + return <Element {...props} group={"actions"} />; +}); + +export type DropdownActionProps = Omit<ElementProps, "group">; + +const BaseDropdownAction = makeDecoratable("TopBarDropdownAction", (props: DropdownActionProps) => { + return <Element {...props} group={"dropdownActions"} />; +}); + +export type ElementsProps = Omit<BaseElementsProps, "scope">; + +const Elements = makeDecoratable("TopBarElements", (props: ElementsProps) => { + return <BaseElements {...props} scope={SCOPE} />; +}); + +export const TopBar = Object.assign(BaseTopBar, { + Element, + Elements, + Layout, + Divider, + Action, + DropdownAction: Object.assign(BaseDropdownAction, { MenuItem }) +}); diff --git a/packages/app-page-builder/src/editor/config/createGetId.ts b/packages/app-page-builder/src/editor/config/createGetId.ts new file mode 100644 index 00000000000..a8f560f05fa --- /dev/null +++ b/packages/app-page-builder/src/editor/config/createGetId.ts @@ -0,0 +1,3 @@ +export const createGetId = (scope: string) => (prefix?: string) => (name: string) => { + return [scope, prefix, name].filter(Boolean).join(":"); +}; diff --git a/packages/app-page-builder/src/editor/config/index.ts b/packages/app-page-builder/src/editor/config/index.ts new file mode 100644 index 00000000000..dc64323a688 --- /dev/null +++ b/packages/app-page-builder/src/editor/config/index.ts @@ -0,0 +1 @@ +export * from "./EditorConfig"; diff --git a/packages/app-page-builder/src/editor/contexts/EventActionHandlerProvider.tsx b/packages/app-page-builder/src/editor/contexts/EventActionHandlerProvider.tsx index 15a4100c8a8..6a2d530b999 100644 --- a/packages/app-page-builder/src/editor/contexts/EventActionHandlerProvider.tsx +++ b/packages/app-page-builder/src/editor/contexts/EventActionHandlerProvider.tsx @@ -15,7 +15,6 @@ import { plugins } from "@webiny/plugins"; import { rootElementAtom, elementsAtom, - pluginsAtom, sidebarAtom, uiAtom, elementByIdSelector, @@ -23,7 +22,6 @@ import { highlightElementAtom, SidebarAtomType, RootElementAtom, - PluginsAtomType, UiAtomType } from "../recoil/modules"; @@ -110,14 +108,12 @@ export const EventActionHandlerProvider = makeDecoratable( const setHighlightElementAtomValue = useSetRecoilState(highlightElementAtom); const [sidebarAtomValue, setSidebarAtomValue] = useRecoilState(sidebarAtom); const rootElementAtomValue = useRecoilValue(rootElementAtom); - const [pluginsAtomValue, setPluginsAtomValue] = useRecoilState(pluginsAtom); const [uiAtomValue, setUiAtomValue] = useRecoilState(uiAtom); const snapshot = useRecoilSnapshot(); const eventActionHandlerRef = useRef<EventActionHandler>(); const sidebarAtomValueRef = useRef<SidebarAtomType>(); const rootElementAtomValueRef = useRef<RootElementAtom>(); - const pluginsAtomValueRef = useRef<PluginsAtomType>(); const uiAtomValueRef = useRef<UiAtomType>(); const snapshotRef = useRef<Snapshot>(); const eventElements = useRef<Record<string, PbEditorElement>>({}); @@ -134,10 +130,9 @@ export const EventActionHandlerProvider = makeDecoratable( useEffect(() => { sidebarAtomValueRef.current = sidebarAtomValue; rootElementAtomValueRef.current = rootElementAtomValue; - pluginsAtomValueRef.current = pluginsAtomValue; uiAtomValueRef.current = uiAtomValue; snapshotRef.current = snapshot; - }, [sidebarAtomValue, rootElementAtomValue, pluginsAtomValue, uiAtomValue, snapshot]); + }, [sidebarAtomValue, rootElementAtomValue, uiAtomValue, snapshot]); const registry = useRef<RegistryType>(new Map()); @@ -272,7 +267,6 @@ export const EventActionHandlerProvider = makeDecoratable( return { sidebar: sidebarAtomValueRef.current as SidebarAtomType, rootElement: rootElementAtomValueRef.current as RootElementAtom, - plugins: pluginsAtomValueRef.current as PluginsAtomType, ui: uiAtomValueRef.current as UiAtomType, getElementById, getElementTree, @@ -317,10 +311,6 @@ export const EventActionHandlerProvider = makeDecoratable( setUiAtomValue(state.ui); } - if (state.plugins) { - setPluginsAtomValue(state.plugins); - } - if (state.hasOwnProperty("activeElement")) { setActiveElementAtomValue(state.activeElement as string); } diff --git a/packages/app-page-builder/src/editor/config/ActionPlugins.tsx b/packages/app-page-builder/src/editor/defaultConfig/ActionPlugins.tsx similarity index 86% rename from packages/app-page-builder/src/editor/config/ActionPlugins.tsx rename to packages/app-page-builder/src/editor/defaultConfig/ActionPlugins.tsx index 4e351c933b7..5d683ea9d18 100644 --- a/packages/app-page-builder/src/editor/config/ActionPlugins.tsx +++ b/packages/app-page-builder/src/editor/defaultConfig/ActionPlugins.tsx @@ -3,12 +3,10 @@ import { plugins } from "@webiny/plugins"; import cloneElementPlugin from "../recoil/actions/cloneElement/plugin"; import createElementPlugin from "../recoil/actions/createElement/plugin"; -import deactivatePluginPlugin from "../recoil/actions/deactivatePlugin/plugin"; import deleteElementPlugin from "../recoil/actions/deleteElement/plugin"; import dragPlugin from "../recoil/actions/drag/plugin"; import dropElementPlugin from "../recoil/actions/dropElement/plugin"; import mirrorCellPlugin from "../recoil/actions/mirrorCell/plugin"; -import togglePluginPlugin from "../recoil/actions/togglePlugin/plugin"; import updateElementPlugin from "../recoil/actions/updateElement/plugin"; import afterDropElementPlugin from "../recoil/actions/afterDropElement/plugin"; import moveBlockPlugin from "../recoil/actions/moveBlock/plugin"; @@ -20,11 +18,9 @@ export const ActionPlugins = memo(() => { cloneElementPlugin(), createElementPlugin(), updateElementPlugin(), - togglePluginPlugin(), dropElementPlugin(), mirrorCellPlugin(), afterDropElementPlugin(), - deactivatePluginPlugin(), deleteElementPlugin(), moveBlockPlugin(), afterUpdateElementsPlugin(), diff --git a/packages/app-page-builder/src/editor/defaultConfig/Content/Background/Background.tsx b/packages/app-page-builder/src/editor/defaultConfig/Content/Background/Background.tsx new file mode 100644 index 00000000000..65dff05dc85 --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Content/Background/Background.tsx @@ -0,0 +1,24 @@ +import React, { useCallback } from "react"; +import { css } from "emotion"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; + +const backgroundStyle = css` + position: fixed; + top: 0; + left: 0; + width: 100%; + min-height: 100%; +`; + +export const Background = () => { + const [activeElement, setActiveElement] = useActiveElement(); + + const deactivateElement = useCallback(() => { + if (!activeElement) { + return; + } + setActiveElement(null); + }, [activeElement]); + + return <div className={backgroundStyle} onClick={deactivateElement} />; +}; diff --git a/packages/app-page-builder/src/editor/config/Background/index.ts b/packages/app-page-builder/src/editor/defaultConfig/Content/Background/index.ts similarity index 100% rename from packages/app-page-builder/src/editor/config/Background/index.ts rename to packages/app-page-builder/src/editor/defaultConfig/Content/Background/index.ts diff --git a/packages/app-page-builder/src/editor/config/Breadcrumbs/Breadcrumbs.tsx b/packages/app-page-builder/src/editor/defaultConfig/Content/Breadcrumbs/Breadcrumbs.tsx similarity index 91% rename from packages/app-page-builder/src/editor/config/Breadcrumbs/Breadcrumbs.tsx rename to packages/app-page-builder/src/editor/defaultConfig/Content/Breadcrumbs/Breadcrumbs.tsx index f703dc6e7e0..c9ffab260d5 100644 --- a/packages/app-page-builder/src/editor/config/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/app-page-builder/src/editor/defaultConfig/Content/Breadcrumbs/Breadcrumbs.tsx @@ -1,17 +1,15 @@ import React, { useCallback, useEffect, useState } from "react"; import { useRecoilCallback, useRecoilSnapshot } from "recoil"; -import { createDecorator } from "@webiny/app-admin"; import { PbEditorElement } from "~/types"; import { breadcrumbs } from "./styles"; import { useActiveElement } from "~/editor/hooks/useActiveElement"; import { useHighlightElement } from "~/editor/hooks/useHighlightElement"; import { elementByIdSelector, elementsAtom, ElementsAtomType } from "~/editor/recoil/modules"; import { useActiveElementId } from "~/editor/hooks/useActiveElementId"; -import { EditorContent } from "~/editor"; type ItemsState = Pick<ElementsAtomType, "id" | "type">; -const Breadcrumbs = () => { +export const Breadcrumbs = () => { const [items, setItems] = useState<ItemsState[]>([]); const [, setActiveElementId] = useActiveElementId(); const [element] = useActiveElement(); @@ -120,14 +118,3 @@ const Breadcrumbs = () => { </ul> ); }; - -export const BreadcrumbsPlugin = createDecorator(EditorContent, PrevContent => { - return function AddBreadcrumbs() { - return ( - <> - <PrevContent /> - <Breadcrumbs /> - </> - ); - }; -}); diff --git a/packages/app-page-builder/src/editor/config/Breadcrumbs/dual-tone-arrow.svg b/packages/app-page-builder/src/editor/defaultConfig/Content/Breadcrumbs/dual-tone-arrow.svg similarity index 100% rename from packages/app-page-builder/src/editor/config/Breadcrumbs/dual-tone-arrow.svg rename to packages/app-page-builder/src/editor/defaultConfig/Content/Breadcrumbs/dual-tone-arrow.svg diff --git a/packages/app-page-builder/src/editor/config/Breadcrumbs/index.ts b/packages/app-page-builder/src/editor/defaultConfig/Content/Breadcrumbs/index.ts similarity index 100% rename from packages/app-page-builder/src/editor/config/Breadcrumbs/index.ts rename to packages/app-page-builder/src/editor/defaultConfig/Content/Breadcrumbs/index.ts diff --git a/packages/app-page-builder/src/editor/config/Breadcrumbs/styles.ts b/packages/app-page-builder/src/editor/defaultConfig/Content/Breadcrumbs/styles.ts similarity index 100% rename from packages/app-page-builder/src/editor/config/Breadcrumbs/styles.ts rename to packages/app-page-builder/src/editor/defaultConfig/Content/Breadcrumbs/styles.ts diff --git a/packages/app-page-builder/src/editor/components/Editor/Content.tsx b/packages/app-page-builder/src/editor/defaultConfig/Content/Elements/Elements.tsx similarity index 50% rename from packages/app-page-builder/src/editor/components/Editor/Content.tsx rename to packages/app-page-builder/src/editor/defaultConfig/Content/Elements/Elements.tsx index ce3cb03f622..fb40ad5b57f 100644 --- a/packages/app-page-builder/src/editor/components/Editor/Content.tsx +++ b/packages/app-page-builder/src/editor/defaultConfig/Content/Elements/Elements.tsx @@ -1,17 +1,9 @@ -import React, { useEffect, useRef, useMemo, useCallback } from "react"; +import React, { useRef } from "react"; import { useRecoilState, useRecoilValue } from "recoil"; import styled from "@emotion/styled"; -import { css } from "emotion"; import kebabCase from "lodash/kebabCase"; -import { Elevation } from "@webiny/ui/Elevation"; import { PbEditorElement, PbTheme } from "~/types"; -import { - uiAtom, - setPagePreviewDimensionMutation, - rootElementAtom, - elementsAtom -} from "~/editor/recoil/modules"; -import { EditorContent } from "~/editor"; +import { uiAtom, rootElementAtom, elementsAtom } from "~/editor/recoil/modules"; import { Element as PeElement } from "@webiny/app-page-builder-elements"; import { Element as ElementType } from "@webiny/app-page-builder-elements/types"; @@ -57,16 +49,6 @@ const ContentContainer = styled(LegacyContentContainer)` } `; -const contentContainerWrapper = css({ - margin: "95px 65px 50px 85px", - padding: 0, - position: "absolute", - width: "calc(100vw - 115px - 300px)", - top: 0, - boxSizing: "border-box", - zIndex: 1 -}); - const LegacyBaseContainer = styled.div` width: 100%; left: 52px; @@ -81,56 +63,23 @@ const BaseContainer = styled(LegacyBaseContainer)` container-name: page-editor-canvas; `; -const Content = () => { +export const Elements = () => { + // TODO: When merging into `next`, for 5.40.0 release, take the changes from `next`! const rootElementId = useRecoilValue(rootElementAtom); const rootElement = useRecoilValue(elementsAtom(rootElementId)) as PbEditorElement; - const [{ displayMode }, setUiAtomValue] = useRecoilState(uiAtom); + const [{ displayMode }] = useRecoilState(uiAtom); const pagePreviewRef = useRef<HTMLDivElement>(null); - const setPagePreviewDimension = useCallback( - pagePreviewDimension => { - setUiAtomValue(prev => setPagePreviewDimensionMutation(prev, pagePreviewDimension)); - }, - [uiAtom] - ); - - const resizeObserver = useMemo(() => { - return new ResizeObserver((entries: ResizeObserverEntry[]) => { - for (const entry of entries) { - const { width, height } = entry.contentRect; - setPagePreviewDimension({ width, height }); - } - }); - }, []); - - // Set resize observer - useEffect(() => { - if (pagePreviewRef.current) { - // Add resize observer - resizeObserver.observe(pagePreviewRef.current); - } - - // Cleanup - return () => { - resizeObserver.disconnect(); - }; - }, []); - return ( - <Elevation className={contentContainerWrapper} z={0}> - <ContentContainer - className={`mdc-elevation--z1 webiny-pb-editor-device--${kebabCase( - displayMode - )} webiny-pb-media-query--${kebabCase(displayMode)}`} - style={{ minHeight: "calc(100vh - 230px)" }} - > - <EditorContent /> - <BaseContainer ref={pagePreviewRef} className={"webiny-pb-editor-content-preview"}> - <PeElement element={rootElement as ElementType} /> - </BaseContainer> - </ContentContainer> - </Elevation> + <ContentContainer + className={`mdc-elevation--z1 webiny-pb-editor-device--${kebabCase( + displayMode + )} webiny-pb-media-query--${kebabCase(displayMode)}`} + style={{ minHeight: "calc(100vh - 230px)" }} + > + <BaseContainer ref={pagePreviewRef} className={"webiny-pb-editor-content-preview"}> + <PeElement element={rootElement as ElementType} /> + </BaseContainer> + </ContentContainer> ); }; - -export default Content; diff --git a/packages/app-page-builder/src/editor/defaultConfig/Content/Elements/index.ts b/packages/app-page-builder/src/editor/defaultConfig/Content/Elements/index.ts new file mode 100644 index 00000000000..6358e573054 --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Content/Elements/index.ts @@ -0,0 +1 @@ +export * from "./Elements"; diff --git a/packages/app-page-builder/src/editor/defaultConfig/DefaultEditorConfig.tsx b/packages/app-page-builder/src/editor/defaultConfig/DefaultEditorConfig.tsx new file mode 100644 index 00000000000..6010a07f8d6 --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/DefaultEditorConfig.tsx @@ -0,0 +1,108 @@ +import React from "react"; +import { ReactComponent as TouchIcon } from "@material-design-icons/svg/round/touch_app.svg"; +import { DisplayModeSelector } from "./TopBar/DisplayModeSelector"; +import { Breadcrumbs } from "./Content/Breadcrumbs"; +import { Background } from "./Content/Background"; +import { Elements } from "./Content/Elements"; +import { ActionPlugins } from "./ActionPlugins"; +import { EditorConfig } from "~/editor/config"; +import { AddElement } from "./Toolbar/AddElement"; +import { Navigator } from "./Toolbar/Navigator"; +import { Saving } from "./Toolbar/Saving/Saving"; +import { Redo, Undo } from "./Toolbar/UndoRedo/UndoRedo"; +import { ElementActions } from "./Sidebar/ElementSettings/ElementActions"; +import { NoActiveElement } from "./Sidebar/NoActiveElement"; +import { InfoMessage } from "./Sidebar/InfoMessage"; +import { OnActiveElement } from "./Sidebar/OnActiveElement"; +import { ElementSettings } from "./Sidebar/ElementSettings/ElementSettings"; +import { StyleSettingsAdapter } from "./Sidebar/BackwardsCompatibility/StyleSettingsAdapter"; +import { StyleSettingsGroup } from "./Sidebar/StyleSettings/StyleSettingsGroup"; +import { StyleProperties } from "./Sidebar/StyleSettings/StyleProperties"; +import { ElementSettingsGroup } from "./Sidebar/ElementSettings/ElementSettingsGroup"; +import { ElementActionsAdapter } from "./Sidebar/BackwardsCompatibility/ElementActionsAdapter"; + +const { TopBar, Content, Toolbar, Sidebar } = EditorConfig; + +const ClickToActivate = () => { + return ( + <NoActiveElement> + <InfoMessage + icon={<TouchIcon />} + message={"Select an element on the canvas to activate this panel."} + /> + </NoActiveElement> + ); +}; + +export const DefaultEditorConfig = React.memo(() => { + return ( + <> + <ActionPlugins /> + <EditorConfig> + <TopBar.Element + name={"displayModeSelector"} + group={"center"} + element={<DisplayModeSelector />} + /> + <Content.Element name={"breadcrumbs"} element={<Breadcrumbs />} /> + <Content.Element name={"background"} element={<Background />} /> + <Content.Element name={"elements"} element={<Elements />} /> + <Toolbar.Element name={"addElement"} group={"top"} element={<AddElement />} /> + <Toolbar.Element name={"navigator"} group={"top"} element={<Navigator />} /> + <Toolbar.Element name={"savingIndicator"} group={"bottom"} element={<Saving />} /> + <Toolbar.Element name={"undo"} group={"bottom"} element={<Undo />} /> + <Toolbar.Element name={"redo"} group={"bottom"} element={<Redo />} /> + {/* Sidebar Groups */} + <Sidebar.Group name={"style"} element={<StyleSettingsGroup />} /> + <Sidebar.Group name={"element"} element={<ElementSettingsGroup />} /> + {/* Style Settings Tab */} + <Sidebar.Element + name={"styleSettings"} + group={"style"} + element={ + <OnActiveElement> + <StyleProperties /> + </OnActiveElement> + } + /> + <Sidebar.Element + name={"styleInactive"} + group={"style"} + element={<ClickToActivate />} + /> + {/* Element Settings Tab */} + <Sidebar.Element + name={"elementInactive"} + group={"element"} + element={<ClickToActivate />} + /> + {/* This element renders element actions. */} + <Sidebar.Element + name={"elementActions"} + group={"element"} + element={ + <OnActiveElement> + <ElementActions /> + </OnActiveElement> + } + /> + {/* This element renders element properties. */} + <Sidebar.Element + name={"elementSettings"} + group={"element"} + element={ + <OnActiveElement> + <ElementSettings /> + </OnActiveElement> + } + /> + {/* This will register style settings from plugins using the new API. */} + <StyleSettingsAdapter /> + {/* This will register actions from plugins using the new API. */} + <ElementActionsAdapter /> + </EditorConfig> + </> + ); +}); + +DefaultEditorConfig.displayName = "DefaultEditorConfig"; diff --git a/packages/app-page-builder/src/editor/config/README.md b/packages/app-page-builder/src/editor/defaultConfig/README.md similarity index 74% rename from packages/app-page-builder/src/editor/config/README.md rename to packages/app-page-builder/src/editor/defaultConfig/README.md index fd34025563b..89122b5005a 100644 --- a/packages/app-page-builder/src/editor/config/README.md +++ b/packages/app-page-builder/src/editor/defaultConfig/README.md @@ -1,2 +1,2 @@ This folder contains the default setup of the editor, which serves as a base for extending. -Implementation of various context-specific features is provided by `blockEditor` and `pageEditor`. +Implementation of various context-specific features is provided by `blockEditor`, `templateEditor`, and `pageEditor`. diff --git a/packages/app-page-builder/src/editor/defaultConfig/Sidebar/BackwardsCompatibility/ElementActionsAdapter.tsx b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/BackwardsCompatibility/ElementActionsAdapter.tsx new file mode 100644 index 00000000000..54f6d223b14 --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/BackwardsCompatibility/ElementActionsAdapter.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import camelCase from "lodash/camelCase"; +import { plugins } from "@webiny/plugins"; +import { PbEditorPageElementSettingsPlugin } from "~/types"; +import { EditorConfig } from "~/editor/config"; +import { useElementSettings } from "~/editor/plugins/elementSettings/hooks/useElementSettings"; + +const getActionName = (pluginName: string) => { + return camelCase(pluginName.replace("pb-editor-page-element-settings-", "")); +}; + +interface ShouldRenderProps { + plugin: PbEditorPageElementSettingsPlugin; + children: React.ReactNode; +} + +/** + * This component runs the same logic we used in the previous implementation of element settings. + * In the previous implementation, we used to calculate actions based on the active element, and render those. + * With this new approach, we always render all actions, and hide them conditionally. + */ +const ShouldRender = ({ plugin, children }: ShouldRenderProps) => { + const elementActions = useElementSettings(); + + const shouldRender = elementActions.find(action => action.plugin.name === plugin.name); + + return shouldRender ? <>{children}</> : null; +}; + +export const ElementActionsAdapter = () => { + const actionPlugins = plugins.byType<PbEditorPageElementSettingsPlugin>( + "pb-editor-page-element-settings" + ); + + return ( + <> + {actionPlugins.map((plugin, index) => { + const element = + typeof plugin.renderAction === "function" ? plugin.renderAction({}) : null; + + return ( + <EditorConfig.Sidebar.ElementAction + key={plugin.name + "-" + index} + name={getActionName(String(plugin.name))} + element={<ShouldRender plugin={plugin}>{element}</ShouldRender>} + /> + ); + })} + </> + ); +}; diff --git a/packages/app-page-builder/src/editor/defaultConfig/Sidebar/BackwardsCompatibility/StyleSettingsAdapter.tsx b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/BackwardsCompatibility/StyleSettingsAdapter.tsx new file mode 100644 index 00000000000..8e1c108bf0a --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/BackwardsCompatibility/StyleSettingsAdapter.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import camelCase from "lodash/camelCase"; +import { plugins } from "@webiny/plugins"; +import { PbEditorPageElementStyleSettingsPlugin } from "~/types"; +import { EditorConfig } from "~/editor/config"; +import { useElementStyleSettings } from "~/editor/plugins/elementSettings/hooks/useElementStyleSettings"; + +const getPropertyName = (pluginName: string) => { + return camelCase(pluginName.replace("pb-editor-page-element-style-settings-", "")); +}; + +interface StyleFromPluginProps { + name: string; + plugin: PbEditorPageElementStyleSettingsPlugin; +} + +const StyleFromPlugin = ({ name, plugin }: StyleFromPluginProps) => { + const elementSettings = useElementStyleSettings(); + + const currentPlugin = elementSettings.find(esPlugin => esPlugin.plugin.name === plugin.name); + + return currentPlugin ? ( + <> + {React.cloneElement(plugin.render({ options: currentPlugin.options }), { + defaultAccordionValue: name === "property" + })} + </> + ) : null; +}; + +export const StyleSettingsAdapter = () => { + const stylePlugins = plugins.byType<PbEditorPageElementStyleSettingsPlugin>( + "pb-editor-page-element-style-settings" + ); + + return ( + <> + {stylePlugins.map(plugin => { + const name = getPropertyName(String(plugin.name)); + return ( + <EditorConfig.Sidebar.ElementProperty + key={plugin.name} + group={EditorConfig.Sidebar.ElementProperty.STYLE_GROUP} + name={name} + element={<StyleFromPlugin name={name} plugin={plugin} />} + /> + ); + })} + <EditorConfig.Sidebar.ElementProperty name={"property"} before={"$first"} /> + </> + ); +}; diff --git a/packages/app-page-builder/src/editor/defaultConfig/Sidebar/ElementSettings/ElementActions.tsx b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/ElementSettings/ElementActions.tsx new file mode 100644 index 00000000000..cbba3692535 --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/ElementSettings/ElementActions.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import styled from "@emotion/styled"; +import { COLORS } from "~/editor/plugins/elementSettings/components/StyledComponents"; +import { EditorConfig } from "~/editor/config"; + +const SidebarActions = styled("div")({ + display: "flex", + flexWrap: "wrap", + borderBottom: `1px solid ${COLORS.gray}`, + backgroundColor: COLORS.lightGray, + borderTop: `1px solid ${COLORS.gray}`, + justifyContent: "center" +}); + +export const ElementActions = () => { + return ( + <SidebarActions> + <EditorConfig.Sidebar.Elements group={"actions"} /> + </SidebarActions> + ); +}; diff --git a/packages/app-page-builder/src/editor/plugins/elementSettings/advanced/ElementSettings.tsx b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/ElementSettings/ElementSettings.tsx similarity index 74% rename from packages/app-page-builder/src/editor/plugins/elementSettings/advanced/ElementSettings.tsx rename to packages/app-page-builder/src/editor/defaultConfig/Sidebar/ElementSettings/ElementSettings.tsx index ec70ad13ec0..0849414f8d0 100644 --- a/packages/app-page-builder/src/editor/plugins/elementSettings/advanced/ElementSettings.tsx +++ b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/ElementSettings/ElementSettings.tsx @@ -3,10 +3,10 @@ import { merge } from "dot-prop-immutable"; import { renderPlugins } from "@webiny/app/plugins"; import { plugins } from "@webiny/plugins"; import { Form, FormOnSubmit, FormRenderPropParams } from "@webiny/form"; -import { PbEditorPageElementAdvancedSettingsPlugin } from "~/types"; +import { PbEditorElement, PbEditorPageElementAdvancedSettingsPlugin } from "~/types"; import { useUpdateElement } from "~/editor/hooks/useUpdateElement"; import { useActiveElement } from "~/editor/hooks/useActiveElement"; -import { makeDecoratable } from "@webiny/app-admin"; +import { Sidebar } from "~/editor/config/Sidebar/Sidebar"; export const ElementSettings = () => { const [element] = useActiveElement(); @@ -37,32 +37,20 @@ export const ElementSettings = () => { <Form key={element && element.id} data={element.data} onSubmit={onSubmit}> {formProps => ( <> - <ElementSettingsRenderer formProps={formProps} /> + <LegacyPlugins element={element} formProps={formProps} /> + <Sidebar.Elements group={"elementProperties"} /> </> )} </Form> ); }; -export const ElementSettingsRenderer = makeDecoratable( - "ElementSettingsRenderer", - // Settings this to `any`, because this API is now deprecated, and we don't want to have it visible. - ({ formProps }: any) => { - return <LegacyPlugins formProps={formProps} />; - } -); - interface LegacyPluginsProps { + element: PbEditorElement; formProps: FormRenderPropParams; } -const LegacyPlugins = ({ formProps }: LegacyPluginsProps) => { - const [element] = useActiveElement(); - - if (!element) { - return null; - } - +const LegacyPlugins = ({ element, formProps }: LegacyPluginsProps) => { return ( <> {renderPlugins<PbEditorPageElementAdvancedSettingsPlugin>( diff --git a/packages/app-page-builder/src/editor/defaultConfig/Sidebar/ElementSettings/ElementSettingsGroup.tsx b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/ElementSettings/ElementSettingsGroup.tsx new file mode 100644 index 00000000000..c967962e93d --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/ElementSettings/ElementSettingsGroup.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { Sidebar } from "~/editor/config/Sidebar/Sidebar"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; + +export const ElementSettingsGroup = () => { + const [element] = useActiveElement(); + + return ( + <Sidebar.Group.Tab + label={"Element"} + element={<Sidebar.Elements group={"element"} />} + disabled={!element} + /> + ); +}; diff --git a/packages/app-page-builder/src/editor/defaultConfig/Sidebar/InfoMessage.tsx b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/InfoMessage.tsx new file mode 100644 index 00000000000..5a0a6fe551b --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/InfoMessage.tsx @@ -0,0 +1,41 @@ +import React, { ReactElement } from "react"; +import styled from "@emotion/styled"; +import { Typography } from "@webiny/ui/Typography"; + +const InfoMessageContainer = styled.div` + padding: 16px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 48px 16px; + background-color: var(--mdc-theme-background); + color: var(--mdc-theme-text-primary-on-background); + max-height: 150px; + & .icon { + fill: var(--mdc-theme-text-icon-on-background); + width: 36px; + height: 36px; + } + & .text { + margin-top: 16px; + color: var(--mdc-theme-text-on-background); + text-align: center; + } +`; + +export interface InfoMessageProps { + message: string; + icon?: ReactElement; +} + +export const InfoMessage = ({ message, icon }: InfoMessageProps) => { + return ( + <InfoMessageContainer> + {icon && React.cloneElement(icon, { className: "icon" })} + <Typography use={"subtitle1"} className={"text"}> + {message} + </Typography> + </InfoMessageContainer> + ); +}; diff --git a/packages/app-page-builder/src/editor/defaultConfig/Sidebar/NoActiveElement.tsx b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/NoActiveElement.tsx new file mode 100644 index 00000000000..3d78c325808 --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/NoActiveElement.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; + +interface NoActiveElementProps { + children?: React.ReactNode; +} + +export const NoActiveElement = ({ children }: NoActiveElementProps) => { + const [element] = useActiveElement(); + + if (element) { + return null; + } + + return <>{children}</>; +}; diff --git a/packages/app-page-builder/src/editor/defaultConfig/Sidebar/OnActiveElement.tsx b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/OnActiveElement.tsx new file mode 100644 index 00000000000..526c7589eb5 --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/OnActiveElement.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; + +interface OnActiveElementProps { + children?: React.ReactNode; +} + +export const OnActiveElement = ({ children }: OnActiveElementProps) => { + const [element] = useActiveElement(); + + if (!element) { + return null; + } + + return <>{children}</>; +}; diff --git a/packages/app-page-builder/src/editor/defaultConfig/Sidebar/StyleSettings/StyleProperties.tsx b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/StyleSettings/StyleProperties.tsx new file mode 100644 index 00000000000..83c92645573 --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/StyleSettings/StyleProperties.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import { Sidebar } from "~/editor/config/Sidebar/Sidebar"; + +export const StyleProperties = () => { + return <Sidebar.Elements group={"styleProperties"} />; +}; diff --git a/packages/app-page-builder/src/editor/defaultConfig/Sidebar/StyleSettings/StyleSettingsGroup.tsx b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/StyleSettings/StyleSettingsGroup.tsx new file mode 100644 index 00000000000..43be9acc014 --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Sidebar/StyleSettings/StyleSettingsGroup.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import { Sidebar } from "~/editor/config/Sidebar/Sidebar"; + +export const StyleSettingsGroup = () => { + return <Sidebar.Group.Tab label={"Style"} element={<Sidebar.Elements group={"style"} />} />; +}; diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/addElement/AddElement.tsx b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/AddElement/AddElementDrawer.tsx similarity index 89% rename from packages/app-page-builder/src/editor/plugins/toolbar/addElement/AddElement.tsx rename to packages/app-page-builder/src/editor/defaultConfig/Toolbar/AddElement/AddElementDrawer.tsx index 2aa332c38e4..c8e17781468 100644 --- a/packages/app-page-builder/src/editor/plugins/toolbar/addElement/AddElement.tsx +++ b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/AddElement/AddElementDrawer.tsx @@ -1,14 +1,11 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { useRecoilValue } from "recoil"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { css } from "emotion"; import isEqual from "lodash/isEqual"; import { Typography } from "@webiny/ui/Typography"; import { ButtonFloating } from "@webiny/ui/Button"; import * as Styled from "./StyledComponents"; -import { activePluginParamsByNameSelector } from "~/editor/recoil/modules"; import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; import { - DeactivatePluginActionEvent, DragEndActionEvent, DragStartActionEvent, DropElementActionEvent @@ -18,12 +15,11 @@ import { plugins } from "@webiny/plugins"; import { usePageBuilder } from "~/hooks/usePageBuilder"; import { ReactComponent as AddIcon } from "~/editor/assets/icons/add.svg"; import { PbEditorPageElementGroupPlugin, PbEditorPageElementPlugin } from "~/types"; -import { useKeyHandler } from "~/editor/hooks/useKeyHandler"; import { DropElementActionArgsType } from "~/editor/recoil/actions/dropElement/types"; import Accordion from "~/editor/plugins/elementSettings/components/Accordion"; import { useSnackbar } from "@webiny/app-admin"; - -const ADD_ELEMENT = "pb-editor-toolbar-add-element"; +import { AddElementButton } from "~/editor/plugins/elements/cell/AddElementButton"; +import { useDrawer } from "~/editor/config/Toolbar/DrawerProvider"; const accordionStyle = css` & .icon-container svg { @@ -76,14 +72,34 @@ const ElementsList = ({ groupPlugin, elements, renderDraggable, refresh }: Eleme ); }; -const AddElement = () => { +export const AddElementDrawer = () => { + const drawer = useDrawer(); + const [params, setParams] = useState<DropElementActionArgsType["target"] | undefined>( + undefined + ); const handler = useEventActionHandler(); - const params = useRecoilValue(activePluginParamsByNameSelector(ADD_ELEMENT)); - const { removeKeyHandler, addKeyHandler } = useKeyHandler(); const elementPlugins = plugins.byType<PbEditorPageElementPlugin>("pb-editor-page-element"); const [elements, setElements] = useState(elementPlugins); const { showSnackbar } = useSnackbar(); + const onAddElement = useCallback(element => { + setParams({ + id: element.id, + type: element.type, + path: element.path?.join("."), + position: 0 + }); + drawer.open(); + }, []); + + const AddElementButtonDecorator = useMemo(() => { + return AddElementButton.createDecorator(Original => { + return function AddElementButton(props) { + return <Original {...props} onClick={onAddElement} />; + }; + }); + }, []); + const refresh = useCallback(() => { setElements(elementPlugins); }, [elementPlugins]); @@ -100,16 +116,13 @@ const AddElement = () => { const dragEnd = useCallback(() => { handler.trigger(new DragEndActionEvent()); }, []); - const deactivatePlugin = useCallback(() => { - handler.trigger( - new DeactivatePluginActionEvent({ - name: ADD_ELEMENT - }) - ); - }, []); + const dropElement = useCallback((args: DropElementActionArgsType) => { handler.trigger(new DropElementActionEvent(args)); + setParams(undefined); + drawer.close(); }, []); + const getGroups = useCallback((): PbEditorPageElementGroupPlugin[] => { const allGroups = plugins.byType<PbEditorPageElementGroupPlugin>( "pb-editor-page-element-group" @@ -181,7 +194,6 @@ const AddElement = () => { target={plugin.target} beginDrag={props => { dragStart(); - setTimeout(deactivatePlugin, 20); return { type: elementType, target: props.target }; }} endDrag={() => { @@ -234,7 +246,7 @@ const AddElement = () => { }, target: params as DropElementActionArgsType["target"] }); - setTimeout(deactivatePlugin, 20); + // setTimeout(deactivatePlugin, 20); } : null, params ? "Click to Add" : "Drag to Add", @@ -245,20 +257,12 @@ const AddElement = () => { </Draggable> ); }, - [params, deactivatePlugin, dropElement, renderOverlay] + [params, dropElement, renderOverlay] ); - useEffect(() => { - addKeyHandler("escape", e => { - e.preventDefault(); - deactivatePlugin(); - }); - - return () => removeKeyHandler("escape"); - }); - return ( <> + <AddElementButtonDecorator /> {pageElementGroupPlugins.map(plugin => ( <Accordion key={plugin.name} @@ -279,5 +283,3 @@ const AddElement = () => { </> ); }; - -export default React.memo(AddElement); diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/addElement/StyledComponents.ts b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/AddElement/StyledComponents.ts similarity index 100% rename from packages/app-page-builder/src/editor/plugins/toolbar/addElement/StyledComponents.ts rename to packages/app-page-builder/src/editor/defaultConfig/Toolbar/AddElement/StyledComponents.ts diff --git a/packages/app-page-builder/src/editor/defaultConfig/Toolbar/AddElement/index.tsx b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/AddElement/index.tsx new file mode 100644 index 00000000000..b202062ef6a --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/AddElement/index.tsx @@ -0,0 +1,36 @@ +import React, { useEffect } from "react"; +import { ReactComponent as AddIcon } from "~/editor/assets/icons/add_circle_outline.svg"; +import { EditorConfig } from "~/editor/config"; +import { useUI } from "~/editor/hooks/useUI"; +import { AddElementDrawer } from "./AddElementDrawer"; +import { useDrawer } from "~/editor/config/Toolbar/DrawerProvider"; + +const { Toolbar } = EditorConfig; + +const HideOnDrag = () => { + const [{ isDragging }] = useUI(); + const { close } = useDrawer(); + + useEffect(() => { + if (isDragging) { + setTimeout(close, 20); + } + }, [isDragging]); + + return null; +}; + +export const AddElement = () => { + return ( + <Toolbar.Element.DrawerTrigger + icon={<AddIcon />} + label={"Add Element"} + drawer={ + <Toolbar.Element.Drawer> + <HideOnDrag /> + <AddElementDrawer /> + </Toolbar.Element.Drawer> + } + /> + ); +}; diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/CollapsableList.tsx b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/CollapsableList.tsx similarity index 97% rename from packages/app-page-builder/src/editor/plugins/toolbar/navigator/CollapsableList.tsx rename to packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/CollapsableList.tsx index c426ac00957..02295e4daf0 100644 --- a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/CollapsableList.tsx +++ b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/CollapsableList.tsx @@ -1,7 +1,7 @@ import React, { useState, useContext, useEffect, ReactElement } from "react"; import classNames from "classnames"; import { EmptyBlock, Collapsable, ArrowRight, HighlightItem } from "./StyledComponents"; -import { NavigatorContext } from "./Navigator"; +import { NavigatorContext } from "./NavigatorDrawer"; interface CollapsableListProps { children: ReactElement | ReactElement[]; diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/DragBlockIndicator.tsx b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/DragBlockIndicator.tsx similarity index 100% rename from packages/app-page-builder/src/editor/plugins/toolbar/navigator/DragBlockIndicator.tsx rename to packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/DragBlockIndicator.tsx diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/Navigator.tsx b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/NavigatorDrawer.tsx similarity index 98% rename from packages/app-page-builder/src/editor/plugins/toolbar/navigator/Navigator.tsx rename to packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/NavigatorDrawer.tsx index 31f4746f960..93bfe0ff44c 100644 --- a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/Navigator.tsx +++ b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/NavigatorDrawer.tsx @@ -29,7 +29,7 @@ export const NavigatorContext = createContext<NavigatorContextValue>({ } }); -const Navigator = () => { +export const NavigatorDrawer = () => { const [elementTree, setElementTree] = useState<PbEditorElement | null>(null); const [expandAll, setExpandAll] = useState<boolean>(false); const [activeElementPath, setActiveElementPath] = useState<string[]>([]); @@ -94,5 +94,3 @@ const Navigator = () => { </NavigatorContext.Provider> ); }; - -export default Navigator; diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/StyledComponents.ts b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/StyledComponents.ts similarity index 100% rename from packages/app-page-builder/src/editor/plugins/toolbar/navigator/StyledComponents.ts rename to packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/StyledComponents.ts diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/TreeView.tsx b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/TreeView.tsx similarity index 99% rename from packages/app-page-builder/src/editor/plugins/toolbar/navigator/TreeView.tsx rename to packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/TreeView.tsx index 2973b4cb47c..9e1b72be796 100644 --- a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/TreeView.tsx +++ b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/TreeView.tsx @@ -11,7 +11,7 @@ import { ElementTypeContainer } from "./StyledComponents"; import CollapsableList from "./CollapsableList"; import DragBlockIndicator from "./DragBlockIndicator"; import { BLOCK, useMoveBlock, useSortableList } from "./navigatorHooks"; -import { NavigatorContext } from "./Navigator"; +import { NavigatorContext } from "./NavigatorDrawer"; import { PbEditorElement } from "~/types"; const elementIdStyle = css` diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/assets/arrow_downward_24px.svg b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/assets/arrow_downward_24px.svg similarity index 100% rename from packages/app-page-builder/src/editor/plugins/toolbar/navigator/assets/arrow_downward_24px.svg rename to packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/assets/arrow_downward_24px.svg diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/assets/arrow_upward_24px.svg b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/assets/arrow_upward_24px.svg similarity index 100% rename from packages/app-page-builder/src/editor/plugins/toolbar/navigator/assets/arrow_upward_24px.svg rename to packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/assets/arrow_upward_24px.svg diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/assets/drag_indicator_24px.svg b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/assets/drag_indicator_24px.svg similarity index 100% rename from packages/app-page-builder/src/editor/plugins/toolbar/navigator/assets/drag_indicator_24px.svg rename to packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/assets/drag_indicator_24px.svg diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/assets/unfold_less_24px.svg b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/assets/unfold_less_24px.svg similarity index 100% rename from packages/app-page-builder/src/editor/plugins/toolbar/navigator/assets/unfold_less_24px.svg rename to packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/assets/unfold_less_24px.svg diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/assets/unfold_more_24px.svg b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/assets/unfold_more_24px.svg similarity index 100% rename from packages/app-page-builder/src/editor/plugins/toolbar/navigator/assets/unfold_more_24px.svg rename to packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/assets/unfold_more_24px.svg diff --git a/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/index.tsx b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/index.tsx new file mode 100644 index 00000000000..046f861e452 --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/index.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { css } from "emotion"; +import { ReactComponent as NavigatorIcon } from "~/editor/assets/icons/segment_black_24px.svg"; +import { EditorConfig } from "~/editor/config"; +import { NavigatorDrawer } from "./NavigatorDrawer"; + +const drawerClassName = css({ + "&.mdc-drawer--dismissible": { + width: "280px !important", + maxWidth: "280px !important" + }, + "&.mdc-drawer": { + height: "calc(100% - 64px)" + } +}); + +const { Toolbar } = EditorConfig; + +export const Navigator = () => { + return ( + <Toolbar.Element.DrawerTrigger + icon={<NavigatorIcon />} + label={"Navigator"} + drawer={ + <Toolbar.Element.Drawer drawerClassName={drawerClassName}> + <NavigatorDrawer /> + </Toolbar.Element.Drawer> + } + /> + ); +}; diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/navigatorHooks.ts b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/navigatorHooks.ts similarity index 100% rename from packages/app-page-builder/src/editor/plugins/toolbar/navigator/navigatorHooks.ts rename to packages/app-page-builder/src/editor/defaultConfig/Toolbar/Navigator/navigatorHooks.ts diff --git a/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Saving/Saving.tsx b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Saving/Saving.tsx new file mode 100644 index 00000000000..88360df94e7 --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/Saving/Saving.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { EditorConfig } from "~/editor/config"; +import { useUI } from "~/editor/hooks/useUI"; +import { ReactComponent as SaveIcon } from "~/editor/assets/icons/baseline-cloud_upload-24px.svg"; +import { ReactComponent as SavedIcon } from "~/editor/assets/icons/baseline-cloud_done-24px.svg"; + +const { Toolbar } = EditorConfig; + +export const Saving = () => { + const [{ isSaving }] = useUI(); + + if (!isSaving) { + return <Toolbar.Element.IconButton label={"All changes saved!"} icon={<SavedIcon />} />; + } + + return <Toolbar.Element.IconButton label={"Saving changes ..."} icon={<SaveIcon />} />; +}; diff --git a/packages/app-page-builder/src/editor/defaultConfig/Toolbar/UndoRedo/UndoRedo.tsx b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/UndoRedo/UndoRedo.tsx new file mode 100644 index 00000000000..c99f7238649 --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/Toolbar/UndoRedo/UndoRedo.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import platform from "platform"; +import { EditorConfig } from "~/editor/config"; +import { ReactComponent as UndoIcon } from "~/editor/assets/icons/undo-icon.svg"; +import { ReactComponent as RedoIcon } from "~/editor/assets/icons/redo-icon.svg"; +import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; + +const osFamily = platform.os ? platform.os.family : null; +const metaKey = osFamily === "OS X" ? "CMD" : "CTRL"; + +const { Toolbar } = EditorConfig; + +export const Undo = () => { + const { undo } = useEventActionHandler(); + const [, setActiveElement] = useActiveElement(); + + const onClick = () => { + undo(); + setActiveElement(null); + }; + return ( + <Toolbar.Element.IconButton + icon={<UndoIcon />} + label={`Undo (${metaKey}+Z)`} + onClick={onClick} + /> + ); +}; + +export const Redo = () => { + const { redo } = useEventActionHandler(); + const [, setActiveElement] = useActiveElement(); + + const onClick = () => { + setActiveElement(null); + redo(); + }; + return ( + <Toolbar.Element.IconButton + icon={<RedoIcon />} + label={`Redo (${metaKey}+SHIFT+Z)`} + onClick={onClick} + /> + ); +}; diff --git a/packages/app-page-builder/src/editor/config/ResponsiveModeSelector/ResponsiveModeSelector.tsx b/packages/app-page-builder/src/editor/defaultConfig/TopBar/DisplayModeSelector/DisplayModeSelector.tsx similarity index 99% rename from packages/app-page-builder/src/editor/config/ResponsiveModeSelector/ResponsiveModeSelector.tsx rename to packages/app-page-builder/src/editor/defaultConfig/TopBar/DisplayModeSelector/DisplayModeSelector.tsx index 2c1ca28e639..5dffa2096b0 100644 --- a/packages/app-page-builder/src/editor/config/ResponsiveModeSelector/ResponsiveModeSelector.tsx +++ b/packages/app-page-builder/src/editor/defaultConfig/TopBar/DisplayModeSelector/DisplayModeSelector.tsx @@ -83,7 +83,7 @@ const classes = { }) }; -export const ResponsiveModeSelector = () => { +export const DisplayModeSelector = () => { const [{ displayMode, pagePreviewDimension }, setUiValue] = useUI(); const { responsiveDisplayMode: { setDisplayMode } diff --git a/packages/app-page-builder/src/editor/defaultConfig/TopBar/DisplayModeSelector/index.ts b/packages/app-page-builder/src/editor/defaultConfig/TopBar/DisplayModeSelector/index.ts new file mode 100644 index 00000000000..c72f3f34eec --- /dev/null +++ b/packages/app-page-builder/src/editor/defaultConfig/TopBar/DisplayModeSelector/index.ts @@ -0,0 +1 @@ +export { DisplayModeSelector } from "./DisplayModeSelector"; diff --git a/packages/app-page-builder/src/editor/hooks/useActivePluginsByType.ts b/packages/app-page-builder/src/editor/hooks/useActivePluginsByType.ts deleted file mode 100644 index 07a86ba58cb..00000000000 --- a/packages/app-page-builder/src/editor/hooks/useActivePluginsByType.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useRecoilValue } from "recoil"; -import { activePluginsByTypeNamesSelector } from "~/editor/recoil/modules"; - -export function useActivePluginsByType(type: string) { - return useRecoilValue(activePluginsByTypeNamesSelector(type)); -} diff --git a/packages/app-page-builder/src/editor/hooks/useKeyHandler.ts b/packages/app-page-builder/src/editor/hooks/useKeyHandler.ts index 972d5a96654..8db23a4a178 100644 --- a/packages/app-page-builder/src/editor/hooks/useKeyHandler.ts +++ b/packages/app-page-builder/src/editor/hooks/useKeyHandler.ts @@ -72,15 +72,15 @@ export function useKeyHandler(): { addKeyHandler: AddKeyHandlerType; removeKeyHandler: RemoveKeyHandlerType; } { - const [id] = React.useState(getNanoid()); + const idRef = React.useRef(getNanoid()); return React.useMemo( () => ({ addKeyHandler(key, handler) { - addKeyHandler(id, key, handler); + addKeyHandler(idRef.current, key, handler); }, removeKeyHandler(key) { - removeKeyHandler(id, key); + removeKeyHandler(idRef.current, key); } }), [] diff --git a/packages/app-page-builder/src/editor/hooks/useRefreshBlock.ts b/packages/app-page-builder/src/editor/hooks/useRefreshBlock.ts index cdcde66dd25..fd7c9b85e40 100644 --- a/packages/app-page-builder/src/editor/hooks/useRefreshBlock.ts +++ b/packages/app-page-builder/src/editor/hooks/useRefreshBlock.ts @@ -4,42 +4,46 @@ import { PbEditorElement } from "~/types"; import { addElementId } from "~/editor/helpers"; import { usePageBlocks } from "~/admin/contexts/AdminPageBuilder/PageBlocks/usePageBlocks"; -export const useRefreshBlock = (block: PbEditorElement) => { +export const useRefreshBlock = () => { const updateElement = useUpdateElement(); const { refetchBlock } = usePageBlocks(); const [loading, setLoading] = useState(false); - const refreshBlock = useCallback(async () => { - if (!block?.id || !block.data.blockId) { - return; - } + const refreshBlock = useCallback( + async (block: PbEditorElement) => { + if (!block?.id || !block.data.blockId) { + return; + } - setLoading(true); - const pageBlock = await refetchBlock(block.data.blockId); + setLoading(true); + const pageBlock = await refetchBlock(block.data.blockId); - const blockDataVariables = pageBlock.content.data.variables || []; - const variables = blockDataVariables.map((blockDataVariable: any) => { - const value = - block.data?.variables?.find((variable: any) => variable.id === blockDataVariable.id) - ?.value || blockDataVariable.value; + const blockDataVariables = pageBlock.content.data.variables || []; + const variables = blockDataVariables.map((blockDataVariable: any) => { + const value = + block.data?.variables?.find( + (variable: any) => variable.id === blockDataVariable.id + )?.value || blockDataVariable.value; - return { - ...blockDataVariable, - value - }; - }); + return { + ...blockDataVariable, + value + }; + }); - updateElement({ - ...addElementId(pageBlock.content), - id: block.id, - data: { - ...block?.data, - ...pageBlock?.content?.data, - variables - } - }); - setLoading(false); - }, [block, updateElement]); + updateElement({ + ...addElementId(pageBlock.content), + id: block.id, + data: { + ...block?.data, + ...pageBlock?.content?.data, + variables + } + }); + setLoading(false); + }, + [updateElement] + ); return { loading, refreshBlock }; }; diff --git a/packages/app-page-builder/src/editor/index.tsx b/packages/app-page-builder/src/editor/index.tsx index 83ee3e6b705..aa1aeeca851 100644 --- a/packages/app-page-builder/src/editor/index.tsx +++ b/packages/app-page-builder/src/editor/index.tsx @@ -9,11 +9,7 @@ * to be provided using the composition API, <EditorConfig> component, and `createStateInitializer` prop. */ export { EditorConfig } from "./components/Editor/EditorConfig"; +export { DefaultEditorConfig } from "./defaultConfig/DefaultEditorConfig"; export * from "./components/Editor/EditorBar"; -export * from "./components/Editor/EditorContent"; export { EditorProvider } from "./contexts/EditorProvider"; -export { EditorSidebarTab, EditorSidebarTabProps } from "./components/Editor/EditorSidebar"; -export { SidebarActions } from "./components/Editor/Sidebar/ElementSettingsTabContent"; -export { ToolbarActions } from "./components/Editor/Toolbar"; -export { ElementSettingsRenderer } from "./plugins/elementSettings/advanced/ElementSettings"; export { default as DropZone } from "../editor/components/DropZone"; diff --git a/packages/app-page-builder/src/editor/plugins/elementSettings/accordion/AccordionItemsList.tsx b/packages/app-page-builder/src/editor/plugins/elementSettings/accordion/AccordionItemsList.tsx index 54393c76703..057ce250ab6 100644 --- a/packages/app-page-builder/src/editor/plugins/elementSettings/accordion/AccordionItemsList.tsx +++ b/packages/app-page-builder/src/editor/plugins/elementSettings/accordion/AccordionItemsList.tsx @@ -8,9 +8,9 @@ import { ReactComponent as AddIcon } from "@webiny/app-admin/assets/icons/add-18 import { activeElementAtom, elementWithChildrenByIdSelector } from "~/editor/recoil/modules"; import Accordion from "~/editor/plugins/elementSettings/components/Accordion"; import { moveInPlace, useSortableList } from "~/hooks/useSortableList"; -import { Collapsable } from "~/editor/plugins/toolbar/navigator/StyledComponents"; +import { Collapsable } from "~/editor/defaultConfig/Toolbar/Navigator/StyledComponents"; import { useUpdateElement } from "~/editor/hooks/useUpdateElement"; -import { ReactComponent as DragIndicatorIcon } from "~/editor/plugins/toolbar/navigator/assets/drag_indicator_24px.svg"; +import { ReactComponent as DragIndicatorIcon } from "~/editor/defaultConfig/Toolbar/Navigator/assets/drag_indicator_24px.svg"; import { ReactComponent as DeleteIcon } from "~/editor/assets/icons/delete.svg"; import { createElement } from "~/editor/helpers"; import { PbEditorElement } from "~/types"; diff --git a/packages/app-page-builder/src/editor/plugins/elementSettings/background/BackgroundSettings.tsx b/packages/app-page-builder/src/editor/plugins/elementSettings/background/BackgroundSettings.tsx index 056c6b43b70..4cb561d338b 100644 --- a/packages/app-page-builder/src/editor/plugins/elementSettings/background/BackgroundSettings.tsx +++ b/packages/app-page-builder/src/editor/plugins/elementSettings/background/BackgroundSettings.tsx @@ -1,6 +1,5 @@ import React, { useCallback, useMemo } from "react"; import { css } from "emotion"; -import { useRecoilValue } from "recoil"; import startCase from "lodash/startCase"; import get from "lodash/get"; import set from "lodash/set"; @@ -10,15 +9,9 @@ import { plugins } from "@webiny/plugins"; import { Cell, Grid } from "@webiny/ui/Grid"; import SingleImageUpload from "@webiny/app-admin/components/SingleImageUpload"; import { - PbEditorElement, PbEditorPageElementSettingsRenderComponentProps, PbEditorResponsiveModePlugin } from "~/types"; -import { - activeElementAtom, - elementWithChildrenByIdSelector, - uiAtom -} from "../../../recoil/modules"; import useUpdateHandlers from "../useUpdateHandlers"; // Components import Wrapper from "../components/Wrapper"; @@ -27,6 +20,8 @@ import Accordion from "../components/Accordion"; import ColorPicker from "../components/ColorPicker"; import { ContentWrapper, classes } from "../components/StyledComponents"; import { applyFallbackDisplayMode } from "../elementSettingsUtils"; +import { useDisplayMode } from "~/editor/hooks/useDisplayMode"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; const positions = [ "top left", @@ -54,13 +49,12 @@ interface SettingsPropsType extends PbEditorPageElementSettingsRenderComponentPr }; } const BackgroundSettings = ({ options, defaultAccordionValue }: SettingsPropsType) => { - const { displayMode } = useRecoilValue(uiAtom); - const activeElementId = useRecoilValue(activeElementAtom); - const element = useRecoilValue( - elementWithChildrenByIdSelector(activeElementId) - ) as PbEditorElement; + const { displayMode } = useDisplayMode(); + const [element] = useActiveElement(); + const { getUpdateValue, getUpdatePreview } = useUpdateHandlers({ - element, + // We know active element must exist for settings to be rendered, so using `!` is ok here. + element: element!, dataNamespace: DATA_NAMESPACE, postModifyElement: ({ newElement }) => { const value = get(newElement, `${DATA_NAMESPACE}.${displayMode}`, {}); diff --git a/packages/app-page-builder/src/editor/plugins/elementSettings/carousel/Carousel.tsx b/packages/app-page-builder/src/editor/plugins/elementSettings/carousel/Carousel.tsx index 7bed31b5fde..31b131140ff 100644 --- a/packages/app-page-builder/src/editor/plugins/elementSettings/carousel/Carousel.tsx +++ b/packages/app-page-builder/src/editor/plugins/elementSettings/carousel/Carousel.tsx @@ -12,8 +12,8 @@ import { ReactComponent as DeleteIcon } from "~/editor/assets/icons/delete.svg"; import { activeElementAtom, elementWithChildrenByIdSelector } from "~/editor/recoil/modules"; import { ReactComponent as AddIcon } from "@webiny/app-admin/assets/icons/add-18px.svg"; import Accordion from "~/editor/plugins/elementSettings/components/Accordion"; -import { Collapsable } from "~/editor/plugins/toolbar/navigator/StyledComponents"; -import { ReactComponent as DragIndicatorIcon } from "~/editor/plugins/toolbar/navigator/assets/drag_indicator_24px.svg"; +import { Collapsable } from "~/editor/defaultConfig/Toolbar/Navigator/StyledComponents"; +import { ReactComponent as DragIndicatorIcon } from "~/editor/defaultConfig/Toolbar/Navigator/assets/drag_indicator_24px.svg"; const accordionStyles = css` .accordion-content { diff --git a/packages/app-page-builder/src/editor/plugins/elementSettings/components/Action.tsx b/packages/app-page-builder/src/editor/plugins/elementSettings/components/Action.tsx index 0e8c41ba508..4d3f4c3ded0 100644 --- a/packages/app-page-builder/src/editor/plugins/elementSettings/components/Action.tsx +++ b/packages/app-page-builder/src/editor/plugins/elementSettings/components/Action.tsx @@ -1,24 +1,10 @@ -import { useEventActionHandler } from "../../../hooks/useEventActionHandler"; -import { TogglePluginActionEvent } from "../../../recoil/actions"; import React, { useEffect, useCallback, ReactElement } from "react"; -import { isPluginActiveSelector, activePluginsByTypeTotalSelector } from "../../../recoil/modules"; -import { css } from "emotion"; import { IconButton } from "@webiny/ui/Button"; -import { useKeyHandler } from "../../../hooks/useKeyHandler"; import { Tooltip } from "@webiny/ui/Tooltip"; -import { useRecoilValue } from "recoil"; - -const editorPageElementSettingsPluginType = "pb-editor-page-element-settings"; - -const activeStyle = css({ - "&.mdc-icon-button": { - color: "var(--mdc-theme-primary)" - } -}); +import { useKeyHandler } from "~/editor/hooks/useKeyHandler"; interface ActionProps { disabled?: boolean; - plugin?: string; icon?: ReactElement; tooltip?: string; onClick?: () => void; @@ -28,7 +14,6 @@ interface ActionProps { } const Action = ({ - plugin, icon, tooltip, onClick, @@ -36,32 +21,17 @@ const Action = ({ disabled = false, ...props }: ActionProps) => { - const eventActionHandler = useEventActionHandler(); - const isPluginActive = useRecoilValue(isPluginActiveSelector(plugin as string)); - const settingsActive = - useRecoilValue(activePluginsByTypeTotalSelector(editorPageElementSettingsPluginType)) > 0; - const { addKeyHandler, removeKeyHandler } = useKeyHandler(); const clickHandler = useCallback((): void => { if (typeof onClick === "function") { return onClick(); } - eventActionHandler.trigger( - new TogglePluginActionEvent({ - name: plugin || "unknown", - closeOtherInGroup: true - }) - ); - }, [plugin, onClick]); + }, [onClick]); useEffect((): (() => void) => { shortcut.map(short => { addKeyHandler(short, e => { - if (settingsActive) { - return; - } - e.preventDefault(); if (!onClick) { return; @@ -78,16 +48,11 @@ const Action = ({ }, [onClick]); return ( - <Tooltip - placement={"bottom"} - content={<span>{tooltip}</span>} - {...(isPluginActive ? { visible: false } : {})} - > + <Tooltip placement={"bottom"} content={<span>{tooltip}</span>}> <IconButton disabled={disabled} icon={icon} onClick={clickHandler} - className={isPluginActive ? activeStyle : ""} data-testid={props["data-testid"]} /> </Tooltip> diff --git a/packages/app-page-builder/src/editor/plugins/elementSettings/delete/index.tsx b/packages/app-page-builder/src/editor/plugins/elementSettings/delete/index.tsx index 8b97b784f20..f507116acd0 100644 --- a/packages/app-page-builder/src/editor/plugins/elementSettings/delete/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/elementSettings/delete/index.tsx @@ -2,7 +2,7 @@ import React from "react"; import { ReactComponent as DeleteIcon } from "../../../assets/icons/delete.svg"; import DeleteAction from "./DeleteAction"; import Action from "../components/Action"; -import { PbEditorPageElementSettingsPlugin } from "../../../../types"; +import { PbEditorPageElementSettingsPlugin } from "~/types"; export default { name: "pb-editor-page-element-settings-delete", diff --git a/packages/app-page-builder/src/editor/plugins/elementSettings/hooks/useDeactivateOnEsc.ts b/packages/app-page-builder/src/editor/plugins/elementSettings/hooks/useDeactivateOnEsc.ts new file mode 100644 index 00000000000..46528d79095 --- /dev/null +++ b/packages/app-page-builder/src/editor/plugins/elementSettings/hooks/useDeactivateOnEsc.ts @@ -0,0 +1,17 @@ +import { useEffect } from "react"; +import { useKeyHandler } from "~/editor/hooks/useKeyHandler"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; + +export function useDeactivateOnEsc() { + const [, setActiveElement] = useActiveElement(); + const { addKeyHandler, removeKeyHandler } = useKeyHandler(); + + useEffect(() => { + addKeyHandler("escape", e => { + e.preventDefault(); + setActiveElement(null); + }); + + return () => removeKeyHandler("escape"); + }); +} diff --git a/packages/app-page-builder/src/editor/plugins/elementSettings/hooks/useElementSettings.tsx b/packages/app-page-builder/src/editor/plugins/elementSettings/hooks/useElementSettings.tsx index aaedfc5a626..4525dfca02f 100644 --- a/packages/app-page-builder/src/editor/plugins/elementSettings/hooks/useElementSettings.tsx +++ b/packages/app-page-builder/src/editor/plugins/elementSettings/hooks/useElementSettings.tsx @@ -1,14 +1,9 @@ -import { useEffect, useCallback } from "react"; -import { useRecoilState, useRecoilValue } from "recoil"; import { plugins } from "@webiny/plugins"; -import { - PbEditorElement, - PbEditorPageElementPlugin, - PbEditorPageElementSettingsPlugin -} from "~/types"; -import { useKeyHandler } from "../../../hooks/useKeyHandler"; +import { PbEditorPageElementPlugin, PbEditorPageElementSettingsPlugin } from "~/types"; import { userElementSettingsPlugins } from "../../../helpers"; -import { activeElementAtom, elementByIdSelector } from "../../../recoil/modules"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; +import { useMemo } from "react"; +import { useDeactivateOnEsc } from "~/editor/plugins/elementSettings/hooks/useDeactivateOnEsc"; interface ElementAction { plugin: PbEditorPageElementSettingsPlugin; @@ -67,31 +62,16 @@ const getElementActions = (plugin?: PbEditorPageElementPlugin): ElementAction[] ); }; -const useElementSettings = (): ElementAction[] => { - const [activeElement, setActiveElementAtomValue] = useRecoilState(activeElementAtom); - const element = useRecoilValue(elementByIdSelector(activeElement as string)) as PbEditorElement; +export function useElementSettings(): ElementAction[] { + useDeactivateOnEsc(); + const [element] = useActiveElement(); const elementType = element ? element.type : undefined; - const deactivateElement = useCallback(() => { - 3; - setActiveElementAtomValue(null); - }, []); - - const { addKeyHandler, removeKeyHandler } = useKeyHandler(); - - useEffect(() => { - addKeyHandler("escape", e => { - e.preventDefault(); - deactivateElement(); - }); - return () => removeKeyHandler("escape"); - }); - - const plugin = plugins - .byType<PbEditorPageElementPlugin>("pb-editor-page-element") - .find(pl => pl.elementType === elementType); + const plugin = useMemo(() => { + return plugins + .byType<PbEditorPageElementPlugin>("pb-editor-page-element") + .find(pl => pl.elementType === elementType); + }, [elementType]); return getElementActions(plugin); -}; - -export default useElementSettings; +} diff --git a/packages/app-page-builder/src/editor/plugins/elementSettings/hooks/useElementStyleSettings.tsx b/packages/app-page-builder/src/editor/plugins/elementSettings/hooks/useElementStyleSettings.tsx index d814b6b2278..38fbdbac6e0 100644 --- a/packages/app-page-builder/src/editor/plugins/elementSettings/hooks/useElementStyleSettings.tsx +++ b/packages/app-page-builder/src/editor/plugins/elementSettings/hooks/useElementStyleSettings.tsx @@ -1,16 +1,14 @@ -import { useCallback, useEffect } from "react"; -import { useRecoilState, useRecoilValue } from "recoil"; import { plugins } from "@webiny/plugins"; import { PbEditorPageElementPlugin, PbEditorPageElementStyleSettingsPlugin } from "~/types"; -import { useKeyHandler } from "../../../hooks/useKeyHandler"; import { userElementStyleSettingsPlugins } from "../../../helpers"; -import { activeElementAtom, elementByIdSelector } from "../../../recoil/modules"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; +import { useDeactivateOnEsc } from "~/editor/plugins/elementSettings/hooks/useDeactivateOnEsc"; -interface ElementAction { +interface ElementSettings { plugin: PbEditorPageElementStyleSettingsPlugin; options: Record<string, any>; } -const getElementActions = (plugin?: PbEditorPageElementPlugin): ElementAction[] => { +const getElementSettings = (plugin?: PbEditorPageElementPlugin): ElementSettings[] => { if (!plugin || !plugin.settings) { return []; } @@ -20,7 +18,7 @@ const getElementActions = (plugin?: PbEditorPageElementPlugin): ElementAction[] ...(plugin.settings as string[]) ]; - const elementActions = pluginSettings + const elementSettings = pluginSettings .map(pl => { if (typeof pl === "string") { return { plugin: plugins.byName(pl), options: {} }; @@ -32,52 +30,29 @@ const getElementActions = (plugin?: PbEditorPageElementPlugin): ElementAction[] return null; }) - .filter(Boolean) as ElementAction[]; + .filter(Boolean) as ElementSettings[]; return ( - elementActions + elementSettings // Eliminate empty plugins - .filter(pl => { - return pl && pl.plugin; - }) + .filter(pl => pl && pl.plugin) // Eliminate plugins other than "PbEditorPageElementStyleSettingsPlugin". - .filter(pl => { - return ( - pl && pl.plugin && pl.plugin.type === "pb-editor-page-element-style-settings" - ); - }) + .filter(pl => pl.plugin.type === "pb-editor-page-element-style-settings") // Eliminate duplicate plugins - .filter( - (pl, index, array) => - array.findIndex(item => item.plugin.name === pl.plugin.name) === index - ) + .filter((pl, index, array) => { + return array.findIndex(item => item.plugin.name === pl.plugin.name) === index; + }) ); }; -const useElementStyleSettings = (): ElementAction[] => { - const [activeElement, setActiveElementAtomValue] = useRecoilState(activeElementAtom); - const element = useRecoilValue(elementByIdSelector(activeElement as string)); - const elementType = element?.type; - - const deactivateElement = useCallback(() => { - setActiveElementAtomValue(null); - }, []); - - const { addKeyHandler, removeKeyHandler } = useKeyHandler(); - - useEffect(() => { - addKeyHandler("escape", e => { - e.preventDefault(); - deactivateElement(); - }); - return () => removeKeyHandler("escape"); - }); +export const useElementStyleSettings = (): ElementSettings[] => { + useDeactivateOnEsc(); + const [element] = useActiveElement(); + const elementType = element ? element.type : undefined; const plugin = plugins .byType<PbEditorPageElementPlugin>("pb-editor-page-element") .find(pl => pl.elementType === elementType); - return getElementActions(plugin); + return getElementSettings(plugin); }; - -export default useElementStyleSettings; diff --git a/packages/app-page-builder/src/editor/plugins/elementSettings/tabs/TabsList.tsx b/packages/app-page-builder/src/editor/plugins/elementSettings/tabs/TabsList.tsx index fb77ea99de2..23314de222d 100644 --- a/packages/app-page-builder/src/editor/plugins/elementSettings/tabs/TabsList.tsx +++ b/packages/app-page-builder/src/editor/plugins/elementSettings/tabs/TabsList.tsx @@ -8,9 +8,9 @@ import { ReactComponent as AddIcon } from "@webiny/app-admin/assets/icons/add-18 import { activeElementAtom, elementWithChildrenByIdSelector } from "~/editor/recoil/modules"; import Accordion from "~/editor/plugins/elementSettings/components/Accordion"; import { moveInPlace, useSortableList } from "~/hooks/useSortableList"; -import { Collapsable } from "~/editor/plugins/toolbar/navigator/StyledComponents"; +import { Collapsable } from "~/editor/defaultConfig/Toolbar/Navigator/StyledComponents"; import { useUpdateElement } from "~/editor/hooks/useUpdateElement"; -import { ReactComponent as DragIndicatorIcon } from "~/editor/plugins/toolbar/navigator/assets/drag_indicator_24px.svg"; +import { ReactComponent as DragIndicatorIcon } from "~/editor/defaultConfig/Toolbar/Navigator/assets/drag_indicator_24px.svg"; import { ReactComponent as DeleteIcon } from "~/editor/assets/icons/delete.svg"; import { createElement } from "~/editor/helpers"; import { PbEditorElement } from "~/types"; diff --git a/packages/app-page-builder/src/editor/plugins/elementSettings/variable/VariableSettings.tsx b/packages/app-page-builder/src/editor/plugins/elementSettings/variable/VariableSettings.tsx index c32e1d8196f..215a56fc6bd 100644 --- a/packages/app-page-builder/src/editor/plugins/elementSettings/variable/VariableSettings.tsx +++ b/packages/app-page-builder/src/editor/plugins/elementSettings/variable/VariableSettings.tsx @@ -18,7 +18,7 @@ const labelStyle = css({ } }); -const VariableSettings = () => { +export const VariableSettings = () => { const [element] = useActiveElement(); const variableRenderers = useMemo(() => { @@ -61,5 +61,3 @@ const VariableSettings = () => { </div> ); }; - -export default VariableSettings; diff --git a/packages/app-page-builder/src/editor/plugins/elements/cell/AddElementButton.tsx b/packages/app-page-builder/src/editor/plugins/elements/cell/AddElementButton.tsx new file mode 100644 index 00000000000..7d3a939b929 --- /dev/null +++ b/packages/app-page-builder/src/editor/plugins/elements/cell/AddElementButton.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { makeDecoratable } from "@webiny/app-admin"; +import { IconButton } from "@webiny/ui/Button"; +import { PbEditorElement } from "~/types"; +import { ReactComponent as AddCircleOutline } from "~/editor/assets/icons/baseline-add_circle-24px.svg"; + +const defaultOnClick = () => { + console.warn(`EmptyCell "onClick" is not implemented!`); +}; + +export interface AddElementButtonProps { + element: PbEditorElement; + className?: string; + icon?: JSX.Element; + onClick?: (element: PbEditorElement) => void; +} + +export const AddElementButton = makeDecoratable( + "AddElementButton", + ({ + icon = <AddCircleOutline />, + element, + onClick = defaultOnClick, + className = "addIcon" + }: AddElementButtonProps) => { + return <IconButton className={className} icon={icon} onClick={() => onClick(element)} />; + } +); diff --git a/packages/app-page-builder/src/editor/plugins/elements/cell/EmptyCell.tsx b/packages/app-page-builder/src/editor/plugins/elements/cell/EmptyCell.tsx index 0bfda9ccc06..a4fc8890dfd 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/cell/EmptyCell.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/cell/EmptyCell.tsx @@ -1,13 +1,10 @@ import React from "react"; import { SetterOrUpdater } from "recoil"; -import { IconButton } from "@webiny/ui/Button"; -import { ReactComponent as AddCircleOutline } from "~/editor/assets/icons/baseline-add_circle-24px.svg"; -import { TogglePluginActionEvent } from "~/editor/recoil/actions"; -import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; import styled from "@emotion/styled"; import { useActiveElementId } from "~/editor/hooks/useActiveElementId"; import { useElementById } from "~/editor/hooks/useElementById"; import { PbEditorElement } from "~/types"; +import { AddElementButton } from "~/editor/plugins/elements/cell/AddElementButton"; const EmptyCellStyled = styled.div<{ isActive: boolean }>` box-sizing: border-box; @@ -33,11 +30,11 @@ const EmptyCellStyled = styled.div<{ isActive: boolean }>` interface EmptyCellProps { element: PbEditorElement; + onClick?: (element: PbEditorElement) => void; } const EmptyCell = ({ element }: EmptyCellProps) => { const [activeElementId] = useActiveElementId(); - const handler = useEventActionHandler(); const isActive = activeElementId === element.id; const [editorElement] = useElementById(element.id) as [ @@ -47,20 +44,9 @@ const EmptyCell = ({ element }: EmptyCellProps) => { const dragEntered = editorElement.dragEntered; - const onAddClick = () => { - handler.trigger( - new TogglePluginActionEvent({ - name: "pb-editor-toolbar-add-element", - params: { id, path, type }, - closeOtherInGroup: true - }) - ); - }; - - const { id, path, type } = element; return ( <EmptyCellStyled isActive={isActive || dragEntered}> - <IconButton icon={<AddCircleOutline />} onClick={onAddClick} /> + <AddElementButton element={element} /> </EmptyCellStyled> ); }; diff --git a/packages/app-page-builder/src/editor/plugins/elements/cell/PeCell.tsx b/packages/app-page-builder/src/editor/plugins/elements/cell/PeCell.tsx index f93e2a24cb7..7f1d522f505 100644 --- a/packages/app-page-builder/src/editor/plugins/elements/cell/PeCell.tsx +++ b/packages/app-page-builder/src/editor/plugins/elements/cell/PeCell.tsx @@ -3,14 +3,11 @@ import { createRenderer, useRenderer, Elements } from "@webiny/app-page-builder- import { Element } from "@webiny/app-page-builder-elements/types"; import { SetterOrUpdater, useRecoilValue } from "recoil"; import { elementWithChildrenByIdSelector } from "~/editor/recoil/modules"; -import { ReactComponent as AddCircleOutline } from "~/editor/assets/icons/baseline-add_circle-24px.svg"; -import { TogglePluginActionEvent } from "~/editor/recoil/actions"; -import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; import styled from "@emotion/styled"; -import { IconButton } from "@webiny/ui/Button"; import { useActiveElementId } from "~/editor/hooks/useActiveElementId"; import { useElementById } from "~/editor/hooks/useElementById"; import { PbEditorElement } from "~/types"; +import { AddElementButton } from "~/editor/plugins/elements/cell/AddElementButton"; const EmptyCell = styled.div<{ isActive: boolean }>` display: flex; @@ -37,7 +34,6 @@ const PeCell = createRenderer( () => { const { getElement } = useRenderer(); const element = getElement(); - const handler = useEventActionHandler(); const [activeElementId] = useActiveElementId(); const isActive = activeElementId === element.id; @@ -52,30 +48,14 @@ const PeCell = createRenderer( elementWithChildrenByIdSelector(element.id) ) as Element; - const onAddClick = () => { - handler.trigger( - new TogglePluginActionEvent({ - name: "pb-editor-toolbar-add-element", - params: { id, path, type }, - closeOtherInGroup: true - }) - ); - }; - const childrenElements = elementWithChildren?.elements; if (Array.isArray(childrenElements) && childrenElements.length > 0) { return <Elements element={elementWithChildren} />; } - const { id, path, type } = element; - return ( <EmptyCell isActive={isActive || dragEntered}> - <IconButton - className={"addIcon"} - icon={<AddCircleOutline />} - onClick={onAddClick} - /> + <AddElementButton element={element} /> </EmptyCell> ); }, diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/Action.tsx b/packages/app-page-builder/src/editor/plugins/toolbar/Action.tsx deleted file mode 100644 index 777b9b61676..00000000000 --- a/packages/app-page-builder/src/editor/plugins/toolbar/Action.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { useCallback } from "react"; -import { useEventActionHandler } from "../../hooks/useEventActionHandler"; -import { TogglePluginActionEvent } from "../../recoil/actions"; -import { isPluginActiveSelector } from "../../recoil/modules"; -import { css } from "emotion"; -import { IconButton } from "@webiny/ui/Button"; -import { Tooltip } from "@webiny/ui/Tooltip"; -import { useRecoilValue } from "recoil"; - -const activeStyle = css({ - ".mdc-icon-button__icon": { - color: "var(--mdc-theme-primary)" - } -}); - -type IconType = React.ReactElement | React.ReactElement[]; - -const getButtonIcon = (icon: IconType, isActive: boolean): React.ReactElement => { - if (Array.isArray(icon)) { - return isActive ? icon[0] : icon[1]; - } - return icon; -}; -interface ActionPropsType { - id?: string; - icon: IconType; - onClick?: () => any; - tooltip?: string; - plugin?: string; - /* - * If set "true", will close all other active plugins of same type. - * */ - closeOtherInGroup?: boolean; -} -const Action = ({ id, icon, onClick, tooltip, plugin, closeOtherInGroup }: ActionPropsType) => { - const handler = useEventActionHandler(); - const isActive = useRecoilValue(isPluginActiveSelector(plugin as string)); - - const togglePlugin = useCallback(() => { - if (!plugin) { - return; - } - handler.trigger( - new TogglePluginActionEvent({ - name: plugin, - closeOtherInGroup: closeOtherInGroup - }) - ); - }, [plugin, closeOtherInGroup]); - - const clickHandler = useCallback(() => { - if (typeof onClick === "function") { - return onClick(); - } - togglePlugin(); - }, [plugin, closeOtherInGroup]); - - const btnIcon = getButtonIcon(icon, isActive); - - const iconButton = ( - <IconButton - id={id} - icon={btnIcon} - onClick={clickHandler} - className={isActive ? activeStyle : ""} - /> - ); - - if (tooltip) { - return ( - <Tooltip - placement={"right"} - content={<span>{tooltip}</span>} - {...(isActive ? { visible: false } : {})} - > - {iconButton} - </Tooltip> - ); - } - - return iconButton; -}; - -export default React.memo(Action); diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/addElement/index.tsx b/packages/app-page-builder/src/editor/plugins/toolbar/addElement/index.tsx index 1ff148d9de3..a5e8781de16 100644 --- a/packages/app-page-builder/src/editor/plugins/toolbar/addElement/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/toolbar/addElement/index.tsx @@ -1,23 +1 @@ -import React from "react"; -import { PbEditorToolbarTopPlugin } from "~/types"; -import Action from "../Action"; -import AddElement from "./AddElement"; -import { ReactComponent as AddIcon } from "~/editor/assets/icons/add_circle_outline.svg"; - -export default { - name: "pb-editor-toolbar-add-element", - type: "pb-editor-toolbar-top", - renderAction() { - return ( - <Action - tooltip={"Add Element"} - plugin={this.name} - icon={<AddIcon />} - closeOtherInGroup={true} - /> - ); - }, - renderDrawer() { - return <AddElement />; - } -} as PbEditorToolbarTopPlugin; +export { dummy as default } from "../dummy"; diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/dummy.ts b/packages/app-page-builder/src/editor/plugins/toolbar/dummy.ts new file mode 100644 index 00000000000..d24f224c675 --- /dev/null +++ b/packages/app-page-builder/src/editor/plugins/toolbar/dummy.ts @@ -0,0 +1,5 @@ +// Toolbar plugins were replaced with the new API in 5.40.0. +// Remove it safely after 5.40.0 (needs project upgrade). +export const dummy = { + type: "dummy" +}; diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/index.ts b/packages/app-page-builder/src/editor/plugins/toolbar/index.ts deleted file mode 100644 index 8197714dc51..00000000000 --- a/packages/app-page-builder/src/editor/plugins/toolbar/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import addElement from "./addElement"; -import saving from "./saving"; -import { undo, redo } from "./undoRedo"; - -export default [addElement, saving, undo, redo]; diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/index.tsx b/packages/app-page-builder/src/editor/plugins/toolbar/navigator/index.tsx index 1a5b26acfd6..73b6d8e402d 100644 --- a/packages/app-page-builder/src/editor/plugins/toolbar/navigator/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/toolbar/navigator/index.tsx @@ -1,38 +1,3 @@ -import React from "react"; -import { css } from "emotion"; -import Action from "../Action"; -import Navigator from "./Navigator"; -import { ReactComponent as NavigatorIcon } from "~/editor/assets/icons/segment_black_24px.svg"; -import { PbEditorToolbarTopPlugin } from "~/types"; +import { dummy } from "../dummy"; -const drawerClassName = css({ - "&.mdc-drawer--dismissible": { - width: "280px !important", - maxWidth: "280px !important" - }, - "&.mdc-drawer": { - height: "calc(100% - 64px)" - } -}); - -export default () => - ({ - name: "pb-editor-toolbar-navigator", - type: "pb-editor-toolbar-top", - renderAction() { - return ( - <Action - tooltip={"Navigator"} - plugin={this.name} - icon={<NavigatorIcon />} - closeOtherInGroup={true} - /> - ); - }, - renderDrawer() { - return <Navigator />; - }, - toolbar: { - drawerClassName - } - } as PbEditorToolbarTopPlugin); +export default () => dummy; diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/saving/Saving.tsx b/packages/app-page-builder/src/editor/plugins/toolbar/saving/Saving.tsx deleted file mode 100644 index 6fa221d69bc..00000000000 --- a/packages/app-page-builder/src/editor/plugins/toolbar/saving/Saving.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; -import { IconButton } from "@webiny/ui/Button"; -import { Tooltip } from "@webiny/ui/Tooltip"; -import { ReactComponent as SaveIcon } from "~/editor/assets/icons/baseline-cloud_upload-24px.svg"; -import { ReactComponent as SavedIcon } from "~/editor/assets/icons/baseline-cloud_done-24px.svg"; -import { useUI } from "~/editor/hooks/useUI"; - -const Saving = () => { - const [{ isSaving }] = useUI(); - if (!isSaving) { - return ( - <Tooltip placement={"right"} content={<span>{"All changes saved"}</span>}> - <IconButton icon={<SavedIcon />} /> - </Tooltip> - ); - } - - return ( - <Tooltip placement={"right"} content={<span>{"Saving changes ..."}</span>}> - <IconButton icon={<SaveIcon />} /> - </Tooltip> - ); -}; - -export default React.memo(Saving); diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/saving/index.tsx b/packages/app-page-builder/src/editor/plugins/toolbar/saving/index.tsx index 09f12869268..a5e8781de16 100644 --- a/packages/app-page-builder/src/editor/plugins/toolbar/saving/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/toolbar/saving/index.tsx @@ -1,11 +1 @@ -import React from "react"; -import Saving from "./Saving"; -import { PbEditorToolbarBottomPlugin } from "~/types"; - -export default { - name: "pb-editor-toolbar-save", - type: "pb-editor-toolbar-bottom", - renderAction() { - return <Saving />; - } -} as PbEditorToolbarBottomPlugin; +export { dummy as default } from "../dummy"; diff --git a/packages/app-page-builder/src/editor/plugins/toolbar/undoRedo/index.tsx b/packages/app-page-builder/src/editor/plugins/toolbar/undoRedo/index.tsx index 5364ddac9f6..cf01972c80e 100644 --- a/packages/app-page-builder/src/editor/plugins/toolbar/undoRedo/index.tsx +++ b/packages/app-page-builder/src/editor/plugins/toolbar/undoRedo/index.tsx @@ -1,56 +1,5 @@ -import React from "react"; -import platform from "platform"; -import { ReactComponent as UndoIcon } from "~/editor/assets/icons/undo-icon.svg"; -import { ReactComponent as RedoIcon } from "~/editor/assets/icons/redo-icon.svg"; -import { PbEditorToolbarBottomPlugin } from "~/types"; -import Action from "../Action"; -import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; -import { useActiveElement } from "~/editor/hooks/useActiveElement"; +import { dummy } from "../dummy"; -const osFamily = platform.os ? platform.os.family : null; -const metaKey = osFamily === "OS X" ? "CMD" : "CTRL"; +export const undo = dummy; -export const undo: PbEditorToolbarBottomPlugin = { - name: "pb-editor-toolbar-undo", - type: "pb-editor-toolbar-bottom", - renderAction() { - const { undo } = useEventActionHandler(); - const [, setActiveElement] = useActiveElement(); - - const onClick = () => { - undo(); - setActiveElement(null); - }; - return ( - <Action - id={"action-undo"} - tooltip={`Undo (${metaKey}+Z)`} - onClick={() => onClick()} - icon={<UndoIcon />} - /> - ); - } -}; - -export const redo: PbEditorToolbarBottomPlugin = { - name: "pb-editor-toolbar-redo", - type: "pb-editor-toolbar-bottom", - renderAction() { - const { redo } = useEventActionHandler(); - const [, setActiveElement] = useActiveElement(); - - const onClick = () => { - setActiveElement(null); - redo(); - }; - - return ( - <Action - id={"action-redo"} - tooltip={`Redo (${metaKey}+SHIFT+Z)`} - onClick={() => onClick()} - icon={<RedoIcon />} - /> - ); - } -}; +export const redo = dummy; diff --git a/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/action.ts b/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/action.ts deleted file mode 100644 index da894d1169b..00000000000 --- a/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/action.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { PluginsAtomType } from "../../modules"; -import { DeactivatePluginActionArgsType } from "./types"; -import { plugins } from "@webiny/plugins"; -import { EventActionCallable, EventActionHandlerActionCallableResponse } from "~/types"; - -const removePlugin = (state: PluginsAtomType, name: string): PluginsAtomType => { - const plugin = plugins.byName(name); - if (!plugin) { - throw new Error(`There is no plugin with name "${name}".`); - } - const activePluginsByType = state[plugin.type] || []; - - return { - ...state, - [plugin.type]: activePluginsByType.filter(activePlugin => activePlugin.name !== plugin.name) - }; -}; - -const deactivatePluginByName = ( - state: PluginsAtomType, - name: string -): EventActionHandlerActionCallableResponse => { - const newState = removePlugin(state, name); - - return { - state: { - plugins: newState - }, - actions: [] - }; -}; - -const deactivatePluginByType = ( - state: PluginsAtomType, - type: string -): EventActionHandlerActionCallableResponse => { - return { - state: { - plugins: { - ...state, - [type]: [] - } - }, - actions: [] - }; -}; - -const deactivatePluginsByName = ( - state: PluginsAtomType, - names: string[] -): EventActionHandlerActionCallableResponse => { - let newState = { - ...state - }; - for (const name of names) { - newState = removePlugin(newState, name); - } - return { - state: { - plugins: newState - }, - actions: [] - }; -}; - -export const deactivatePluginAction: EventActionCallable<DeactivatePluginActionArgsType> = ( - { plugins: pluginsState }, - _, - args -) => { - if (!args) { - return { - actions: [] - }; - } - const { name, names, type } = args; - if (name) { - return deactivatePluginByName(pluginsState, name); - } else if (type) { - return deactivatePluginByType(pluginsState, type); - } else if (names) { - return deactivatePluginsByName(pluginsState, names); - } - throw new Error("You are trying to deactivate a plugin but did not pass info on which one."); -}; diff --git a/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/event.ts b/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/event.ts deleted file mode 100644 index 4b5974400e0..00000000000 --- a/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/event.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { DeactivatePluginActionArgsType } from "./types"; -import { BaseEventAction } from "../../eventActions"; - -export class DeactivatePluginActionEvent extends BaseEventAction<DeactivatePluginActionArgsType> { - public getName(): string { - return "DeactivatePluginActionEvent"; - } -} diff --git a/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/index.ts b/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/index.ts deleted file mode 100644 index ce1d8471f4b..00000000000 --- a/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./action"; -export * from "./event"; diff --git a/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/plugin.ts b/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/plugin.ts deleted file mode 100644 index 22fa5a3fb8e..00000000000 --- a/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/plugin.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { DeactivatePluginActionEvent } from "./event"; -import { deactivatePluginAction } from "./action"; -import { PbEditorEventActionPlugin } from "~/types"; - -export default (): PbEditorEventActionPlugin => { - return { - type: "pb-editor-event-action-plugin", - name: "pb-editor-event-action-deactivate-plugin", - onEditorMount: handler => { - return handler.on(DeactivatePluginActionEvent, deactivatePluginAction); - } - }; -}; diff --git a/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/types.ts b/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/types.ts deleted file mode 100644 index 3028858fa74..00000000000 --- a/packages/app-page-builder/src/editor/recoil/actions/deactivatePlugin/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface DeactivatePluginActionArgsType { - name?: string; - type?: string; - names?: string[]; -} diff --git a/packages/app-page-builder/src/editor/recoil/actions/index.ts b/packages/app-page-builder/src/editor/recoil/actions/index.ts index 4c564aa37ab..966d451b44a 100644 --- a/packages/app-page-builder/src/editor/recoil/actions/index.ts +++ b/packages/app-page-builder/src/editor/recoil/actions/index.ts @@ -1,11 +1,9 @@ export * from "./createElement"; export * from "./cloneElement"; -export * from "./deactivatePlugin"; export * from "./deleteElement"; export * from "./drag"; export * from "./dropElement"; export * from "./moveBlock"; -export * from "./togglePlugin"; export * from "./updateElement"; export * from "./updateElementTree"; export * from "./updateDocument"; diff --git a/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/action.ts b/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/action.ts deleted file mode 100644 index c020248157f..00000000000 --- a/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/action.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { TogglePluginActionArgsType } from "./types"; -import { plugins } from "@webiny/plugins"; -import { EventActionCallable } from "~/types"; - -export const togglePluginAction: EventActionCallable<TogglePluginActionArgsType> = ( - state, - _, - args -) => { - if (!args) { - return { - actions: [] - }; - } - const { name, params, closeOtherInGroup = false } = args; - const plugin = plugins.byName(name); - if (!plugin) { - throw new Error(`There is no plugin with name "${name}".`); - } - const { plugins: pluginsAtomValue } = state; - const activePluginsByType = pluginsAtomValue[plugin.type] || []; - const isAlreadyActive = activePluginsByType.some(pl => pl.name === name); - - let newPluginsList; - if (isAlreadyActive) { - newPluginsList = activePluginsByType.filter(pl => pl.name !== name); - } else if (closeOtherInGroup) { - newPluginsList = [{ name, params }]; - } else { - newPluginsList = activePluginsByType.concat([{ name, params }]); - } - - return { - state: { - plugins: { - ...pluginsAtomValue, - [plugin.type]: newPluginsList - } - }, - actions: [] - }; -}; diff --git a/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/event.ts b/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/event.ts deleted file mode 100644 index 46bc2d652ae..00000000000 --- a/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/event.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TogglePluginActionArgsType } from "./types"; -import { BaseEventAction } from "../../eventActions"; - -export class TogglePluginActionEvent extends BaseEventAction<TogglePluginActionArgsType> { - public getName(): string { - return "TogglePluginActionEvent"; - } -} diff --git a/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/index.ts b/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/index.ts deleted file mode 100644 index ce1d8471f4b..00000000000 --- a/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./action"; -export * from "./event"; diff --git a/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/plugin.ts b/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/plugin.ts deleted file mode 100644 index f1ca4102878..00000000000 --- a/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/plugin.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { TogglePluginActionEvent } from "./event"; -import { togglePluginAction } from "./action"; -import { PbEditorEventActionPlugin } from "~/types"; - -export default (): PbEditorEventActionPlugin => { - return { - type: "pb-editor-event-action-plugin", - name: "pb-editor-event-action-toggle-plugin", - onEditorMount: handler => { - return handler.on(TogglePluginActionEvent, togglePluginAction); - } - }; -}; diff --git a/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/types.ts b/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/types.ts deleted file mode 100644 index f447a39cf2b..00000000000 --- a/packages/app-page-builder/src/editor/recoil/actions/togglePlugin/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { PluginsAtomPluginParamsType } from "../../modules"; - -export interface TogglePluginActionArgsType { - name: string; - params?: PluginsAtomPluginParamsType; - closeOtherInGroup?: boolean; -} diff --git a/packages/app-page-builder/src/editor/recoil/modules/index.ts b/packages/app-page-builder/src/editor/recoil/modules/index.ts index fb09d21f2a6..2add92425b1 100644 --- a/packages/app-page-builder/src/editor/recoil/modules/index.ts +++ b/packages/app-page-builder/src/editor/recoil/modules/index.ts @@ -1,4 +1,3 @@ export * from "./elements"; -export * from "./plugins"; export * from "./ui"; export * from "./rootElement"; diff --git a/packages/app-page-builder/src/editor/recoil/modules/plugins/index.ts b/packages/app-page-builder/src/editor/recoil/modules/plugins/index.ts deleted file mode 100644 index 0c0cf74fe4f..00000000000 --- a/packages/app-page-builder/src/editor/recoil/modules/plugins/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./mutations"; -export * from "./pluginsAtom"; -export * from "./selectors"; diff --git a/packages/app-page-builder/src/editor/recoil/modules/plugins/mutations/activatePluginMutation.ts b/packages/app-page-builder/src/editor/recoil/modules/plugins/mutations/activatePluginMutation.ts deleted file mode 100644 index 471ef6bc798..00000000000 --- a/packages/app-page-builder/src/editor/recoil/modules/plugins/mutations/activatePluginMutation.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { PluginsAtomType } from "../.."; -import { plugins } from "@webiny/plugins"; -import { EventActionHandlerMutationActionCallable } from "~/types"; - -export const activatePluginByNameMutation: EventActionHandlerMutationActionCallable< - PluginsAtomType, - string -> = (state, name) => { - const pl = plugins.byName(name); - if (!pl) { - return state; - } - const { type } = pl; - if (!type) { - return state; - } - const allPluginsByType = state[type] || []; - const exists = allPluginsByType.some(pl => pl.name === name); - if (exists) { - return state; - } - return { - ...state, - [type]: allPluginsByType.concat([{ name }]) - }; -}; diff --git a/packages/app-page-builder/src/editor/recoil/modules/plugins/mutations/deactivatePluginMutation.ts b/packages/app-page-builder/src/editor/recoil/modules/plugins/mutations/deactivatePluginMutation.ts deleted file mode 100644 index e96e729367f..00000000000 --- a/packages/app-page-builder/src/editor/recoil/modules/plugins/mutations/deactivatePluginMutation.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Plugin } from "@webiny/plugins/types"; -import { PluginsAtomType } from "../pluginsAtom"; -import { plugins } from "@webiny/plugins"; -import { EventActionHandlerMutationActionCallable } from "~/types"; - -export const deactivatePluginByNameMutation: EventActionHandlerMutationActionCallable< - PluginsAtomType, - string -> = (state, name) => { - const target = plugins.byName(name); - if (!target) { - return state; - } - return deactivatePluginMutation(state, target); -}; - -export const deactivatePluginMutation: EventActionHandlerMutationActionCallable< - PluginsAtomType, - Plugin -> = (state, target) => { - if (!target) { - return state; - } - const { type, name } = target; - const allPluginsByType = state[type]; - if (!allPluginsByType || allPluginsByType.length === 0) { - return state; - } - const filteredPluginsByType = allPluginsByType.filter(pl => pl.name !== name); - if (filteredPluginsByType.length === allPluginsByType.length) { - return state; - } - return { - ...state, - [type]: filteredPluginsByType - }; -}; diff --git a/packages/app-page-builder/src/editor/recoil/modules/plugins/mutations/index.ts b/packages/app-page-builder/src/editor/recoil/modules/plugins/mutations/index.ts deleted file mode 100644 index c53662cb838..00000000000 --- a/packages/app-page-builder/src/editor/recoil/modules/plugins/mutations/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./activatePluginMutation"; -export * from "./deactivatePluginMutation"; diff --git a/packages/app-page-builder/src/editor/recoil/modules/plugins/pluginsAtom.ts b/packages/app-page-builder/src/editor/recoil/modules/plugins/pluginsAtom.ts deleted file mode 100644 index 12411faff69..00000000000 --- a/packages/app-page-builder/src/editor/recoil/modules/plugins/pluginsAtom.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { atom } from "recoil"; - -export interface PluginsAtomPluginParamsType { - [key: string]: any; -} -export interface PluginsAtomPluginType { - name: string; - params?: PluginsAtomPluginParamsType; -} -export interface PluginsAtomType { - [key: string]: PluginsAtomPluginType[]; -} -export const pluginsAtom = atom<PluginsAtomType>({ - key: "pluginsAtom", - default: {} -}); diff --git a/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/activePluginParamsByNameSelector.ts b/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/activePluginParamsByNameSelector.ts deleted file mode 100644 index 29483ff35ca..00000000000 --- a/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/activePluginParamsByNameSelector.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { plugins } from "@webiny/plugins"; -import { selectorFamily } from "recoil"; -import { pluginsAtom, PluginsAtomPluginParamsType } from "../.."; - -export const activePluginParamsByNameSelector = selectorFamily< - PluginsAtomPluginParamsType | null, - string ->({ - key: "activePluginParamsByNameSelector", - get: (name: string) => { - return ({ get }) => { - const pl = plugins.byName(name); - if (!pl) { - return null; - } - const { type } = pl; - if (!type) { - return null; - } - const pluginsAtomValue = get(pluginsAtom); - const pluginsByType = pluginsAtomValue[type] || []; - const activePlugin = pluginsByType.find(pl => pl.name === name); - if (!activePlugin) { - return null; - } - return activePlugin.params || null; - }; - } -}); diff --git a/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/activePluginsByTypeNamesSelector.ts b/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/activePluginsByTypeNamesSelector.ts deleted file mode 100644 index dda68f7e92d..00000000000 --- a/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/activePluginsByTypeNamesSelector.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { selectorFamily } from "recoil"; -import { pluginsAtom } from "../pluginsAtom"; - -export const activePluginsByTypeNamesSelector = selectorFamily<string[], string>({ - key: "activePluginsByTypeNamesSelector", - get: type => { - return ({ get }) => { - const activePlugins = get(pluginsAtom); - const pluginsByType = activePlugins[type]; - if (!pluginsByType || pluginsByType.length === 0) { - return []; - } - return pluginsByType.map(p => p.name); - }; - } -}); diff --git a/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/activePluginsByTypeTotalSelector.ts b/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/activePluginsByTypeTotalSelector.ts deleted file mode 100644 index d389a1e03f7..00000000000 --- a/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/activePluginsByTypeTotalSelector.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { selectorFamily } from "recoil"; -import { pluginsAtom } from "../pluginsAtom"; - -export const activePluginsByTypeTotalSelector = selectorFamily<number, string>({ - key: "activePluginsByTypeTotalSelector", - get: type => { - return ({ get }) => { - const activePlugins = get(pluginsAtom); - return (activePlugins[type] || []).length; - }; - } -}); diff --git a/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/index.ts b/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/index.ts deleted file mode 100644 index b4abc58816d..00000000000 --- a/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./activePluginParamsByNameSelector"; -export * from "./activePluginsByTypeNamesSelector"; -export * from "./activePluginsByTypeTotalSelector"; -export * from "./isPluginActiveSelector"; diff --git a/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/isPluginActiveSelector.ts b/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/isPluginActiveSelector.ts deleted file mode 100644 index 01d4734ffe3..00000000000 --- a/packages/app-page-builder/src/editor/recoil/modules/plugins/selectors/isPluginActiveSelector.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { plugins } from "@webiny/plugins"; -import { Plugin } from "@webiny/plugins/types"; -import { selectorFamily } from "recoil"; -import { pluginsAtom, PluginsAtomType } from "../pluginsAtom"; - -export const isPluginActiveSelector = selectorFamily<boolean, string | undefined>({ - key: "isPluginActiveSelector", - get: name => { - return ({ get }) => { - if (!name) { - return false; - } - const target = plugins.byName(name); - const state = get(pluginsAtom); - if (!target) { - return false; - } - return isPluginActive(state, target); - }; - } -}); - -export const isPluginActive = (state: PluginsAtomType, target: Plugin): boolean => { - const { type } = target; - if (!state[type]) { - return false; - } - const list = state[type]; - return list.some(({ name }) => { - return name === target.name; - }); -}; diff --git a/packages/app-page-builder/src/editor/recoil/modules/types.ts b/packages/app-page-builder/src/editor/recoil/modules/types.ts index 6c496ee1906..4fd4fe927a7 100644 --- a/packages/app-page-builder/src/editor/recoil/modules/types.ts +++ b/packages/app-page-builder/src/editor/recoil/modules/types.ts @@ -1,4 +1,3 @@ -import { PluginsAtomType } from "./plugins"; import { ActiveElementAtomType, HighlightElementAtomType, SidebarAtomType, UiAtomType } from "./ui"; import { PbEditorElement } from "~/types"; @@ -6,7 +5,6 @@ export type PbState<TState = unknown> = { activeElement?: ActiveElementAtomType; highlightElement?: HighlightElementAtomType; elements?: { [id: string]: PbEditorElement }; - plugins: PluginsAtomType; ui: UiAtomType; rootElement: string; sidebar: SidebarAtomType; diff --git a/packages/app-page-builder/src/index.ts b/packages/app-page-builder/src/index.ts index 311b75ac234..81c52ad07c4 100644 --- a/packages/app-page-builder/src/index.ts +++ b/packages/app-page-builder/src/index.ts @@ -1,4 +1,7 @@ export * from "./PageBuilder"; +export * from "./blockEditor/editorConfig/BlockEditorConfig"; +export * from "./templateEditor/editorConfig/TemplateEditorConfig"; +export * from "./pageEditor/editorConfig/PageEditorConfig"; // Export extension components export * from "./modules/WebsiteSettings/AddPbWebsiteSettings"; diff --git a/packages/app-page-builder/src/pageEditor/Editor.tsx b/packages/app-page-builder/src/pageEditor/Editor.tsx index 28af9404dc0..33303b1c6f4 100644 --- a/packages/app-page-builder/src/pageEditor/Editor.tsx +++ b/packages/app-page-builder/src/pageEditor/Editor.tsx @@ -30,10 +30,11 @@ import { PbErrorResponse, PbBlockCategory, PbEditorElement } from "~/types"; import createBlockCategoryPlugin from "~/admin/utils/createBlockCategoryPlugin"; import { PageWithContent, RevisionsAtomType } from "~/pageEditor/state"; import { createStateInitializer } from "./createStateInitializer"; -import { PageEditorConfig } from "./config/PageEditorConfig"; import elementVariableRendererPlugins from "~/editor/plugins/elementVariables"; import { useNavigatePage } from "~/admin/hooks/useNavigatePage"; import { usePageBlocks } from "~/admin/contexts/AdminPageBuilder/PageBlocks/usePageBlocks"; +import { DefaultEditorConfig } from "~/editor"; +import { DefaultPageEditorConfig } from "~/pageEditor/config/DefaultPageEditorConfig"; interface PageDataAndRevisionsState { page: PageWithContent | null; @@ -183,7 +184,8 @@ export const PageEditor = () => { return ( <React.Suspense fallback={<EditorLoadingScreen />}> <PageProvider page={page as Page}> - <PageEditorConfig /> + <DefaultEditorConfig /> + <DefaultPageEditorConfig /> <LoadData> <PbEditor stateInitializerFactory={createStateInitializer(page!, revisions)} /> </LoadData> diff --git a/packages/app-page-builder/src/pageEditor/config/BlockElementSidebarPlugin.tsx b/packages/app-page-builder/src/pageEditor/config/BlockElementSidebarPlugin.tsx deleted file mode 100644 index 9f0ff54c965..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/BlockElementSidebarPlugin.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React, { useEffect } from "react"; -import styled from "@emotion/styled"; -import { EditorSidebarTab } from "~/editor"; -import { createDecorator } from "@webiny/app-admin"; -import { useActiveElement } from "~/editor/hooks/useActiveElement"; -import { ButtonPrimary } from "@webiny/ui/Button"; -import UnlinkBlockAction from "~/pageEditor/plugins/elementSettings/UnlinkBlockAction"; -import { ReactComponent as InfoIcon } from "@webiny/app-admin/assets/icons/info.svg"; -import { useElementSidebar } from "~/editor/hooks/useElementSidebar"; -import { useTemplateMode } from "~/pageEditor/hooks/useTemplateMode"; -import { updateSidebarActiveTabIndexMutation } from "~/editor/recoil/modules"; -import { RootElement } from "~/editor/components/Editor/Sidebar/ElementSettingsTabContent"; - -type UnlinkBlockWrapperProps = { - permission: boolean; -}; - -const UnlinkBlockWrapper = styled("div")<UnlinkBlockWrapperProps>` - padding: 16px; - display: grid; - row-gap: 16px; - justify-content: center; - align-items: center; - margin: auto 16px 16px 16px; - text-align: center; - background-color: var(--mdc-theme-background); - border: 3px dashed var(--webiny-theme-color-border); - border-radius: 5px; - opacity: ${props => !props.permission && "0.5"}; - - & .button-wrapper { - font-weight: bold; - } - - & .info-wrapper { - display: flex; - align-items: center; - font-size: 10px; - - & svg { - width: 18px; - margin-right: 5px; - } - } -`; - -type UnlinkTabProps = { - permission: boolean; -}; - -const UnlinkTab = ({ permission }: UnlinkTabProps) => { - return ( - <RootElement> - <UnlinkBlockWrapper permission={permission}> - This is a block element - to change it you need to unlink it first. By unlinking it, - any changes made to the block will no longer automatically reflect to this page. - <div className="button-wrapper"> - {permission ? ( - <UnlinkBlockAction> - <ButtonPrimary>Unlink block</ButtonPrimary> - </UnlinkBlockAction> - ) : ( - "No permissions" - )} - </div> - <div className="info-wrapper"> - <InfoIcon /> Click here to learn more about how block work - </div> - </UnlinkBlockWrapper> - </RootElement> - ); -}; - -export const BlockElementSidebarPlugin = createDecorator(EditorSidebarTab, Tab => { - return function ElementTab({ children, ...props }) { - const [element] = useActiveElement(); - const [sidebar, setSidebar] = useElementSidebar(); - const [isTemplateMode] = useTemplateMode(); - - const unlinkPermission = true; - // TODO: check if the above check even works. - // const unlinkPermission = useMemo((): boolean => { - // const permission = getPermission<PageBuilderSecurityPermission>("pb.block.unlink"); - // if (permission?.name === "*" || permission?.name === "pb.*") { - // return true; - // } - // return Boolean(permission); - // }, [identity]); - - const isReferenceBlock = - element !== null && element.type === "block" && !!element.data?.blockId; - const isStyleTab = props?.label === "Style"; - - useEffect(() => { - if (isReferenceBlock) { - setSidebar(prev => updateSidebarActiveTabIndexMutation(prev, 1)); - } else if (sidebar.activeTabIndex === 1) { - setSidebar(prev => updateSidebarActiveTabIndexMutation(prev, 0)); - } - }, [element?.id]); - - if (isTemplateMode) { - return isStyleTab ? null : <>{children}</>; - } - - return ( - <Tab {...props}> - {isReferenceBlock && isStyleTab ? ( - <UnlinkTab permission={unlinkPermission} /> - ) : ( - children - )} - </Tab> - ); - }; -}); diff --git a/packages/app-page-builder/src/pageEditor/config/DefaultPageEditorConfig.tsx b/packages/app-page-builder/src/pageEditor/config/DefaultPageEditorConfig.tsx new file mode 100644 index 00000000000..5c387674234 --- /dev/null +++ b/packages/app-page-builder/src/pageEditor/config/DefaultPageEditorConfig.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { EventActionHandlerPlugin, EventActionPlugins } from "./eventActions"; +import { PageEditorConfig } from "~/pageEditor/editorConfig/PageEditorConfig"; +import { AddBlock } from "~/templateEditor/config/Content/BlocksBrowser/AddBlock"; +import { AddContent } from "~/templateEditor/config/Content/BlocksBrowser/AddContent"; +import { BackButton } from "./TopBar/BackButton/BackButton"; +import { BlocksBrowser } from "~/templateEditor/config/Content/BlocksBrowser/BlocksBrowser"; +import { EditBlockAction } from "~/templateEditor/config/Sidebar/EditBlockAction"; +import { RefreshBlockAction } from "~/templateEditor/config/Sidebar/RefreshBlockAction"; +import { UnlinkBlock } from "./Sidebar/UnlinkBlock"; +import { Title } from "./TopBar/Title/Title"; +import { PublishPageButton } from "./TopBar/PublishPageButton/PublishPageButton"; +import { PageSettingsButton } from "./TopBar/PageSettings/PageSettingsButton"; +import { PageSettingsOverlay } from "./TopBar/PageSettings/PageSettings"; +import { RevisionsDropdownMenu } from "./TopBar/Revisions/Revisions"; +import { PageOptionsDropdown } from "./TopBar/PageOptionsDropdown/PageOptionsDropdown"; +import { HideSaveAction } from "~/templateEditor/config/Sidebar/HideSaveAction"; +import { VariableSettings } from "./Sidebar/VariableSettings"; +import { TemplateMode } from "./Sidebar/TemplateMode"; +import { UnlinkTemplate } from "./Toolbar/UnlinkTemplate"; +import { PreviewPageOption } from "./TopBar/PreviewPageOption/PreviewPageOption"; +import { SetAsHomepageOption } from "./TopBar/SetAsHomepageOption/SetAsHomepageOption"; + +const { TopBar, Content, Sidebar, Element } = PageEditorConfig; + +export const DefaultPageEditorConfig = React.memo(() => { + return ( + <> + <EventActionHandlerPlugin /> + <EventActionPlugins /> + <PageEditorConfig> + <TopBar.Element name={"buttonBack"} group={"left"} element={<BackButton />} /> + <TopBar.Element name={"title"} group={"left"} element={<Title />} /> + <TopBar.Action name={"dropdownMenuRevisions"} element={<RevisionsDropdownMenu />} /> + <TopBar.Action name={"divider"} element={<TopBar.Divider />} /> + <TopBar.Action name={"buttonPageSettings"} element={<PageSettingsButton />} /> + <TopBar.Action name={"dropdownActions"} element={<PageOptionsDropdown />} /> + <TopBar.Action name={"buttonPublishPage"} element={<PublishPageButton />} /> + <TopBar.DropdownAction name={"previewPage"} element={<PreviewPageOption />} /> + <TopBar.DropdownAction name={"setAsHomepage"} element={<SetAsHomepageOption />} /> + <Element + group={"overlays"} + name={"pageSettings"} + element={<PageSettingsOverlay />} + /> + <Content.Element name={"addBlock"} element={<AddBlock />} /> + <Content.Element name={"addContent"} element={<AddContent />} /> + <Element group={"overlays"} name={"blocksBrowser"} element={<BlocksBrowser />} /> + <Sidebar.ElementAction name={"editBlock"} element={<EditBlockAction />} /> + <Sidebar.ElementAction name={"refreshBlock"} element={<RefreshBlockAction />} /> + + <Sidebar.Element + name={"variableSettings"} + group={"element"} + element={<VariableSettings />} + after={"elementActions"} + /> + <UnlinkBlock /> + <HideSaveAction /> + <TemplateMode /> + <UnlinkTemplate /> + </PageEditorConfig> + </> + ); +}); + +DefaultPageEditorConfig.displayName = "DefaultPageEditorConfig"; diff --git a/packages/app-page-builder/src/pageEditor/config/ElementSettingsTabContentPlugin.tsx b/packages/app-page-builder/src/pageEditor/config/ElementSettingsTabContentPlugin.tsx deleted file mode 100644 index b5c1001b3fa..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/ElementSettingsTabContentPlugin.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from "react"; -import { ReactComponent as EditIcon } from "@material-design-icons/svg/round/edit.svg"; -import { ReactComponent as RefreshIcon } from "@material-design-icons/svg/round/refresh.svg"; -import { SidebarActions } from "~/editor"; -import { createDecorator } from "@webiny/app-admin"; -import Action from "~/editor/plugins/elementSettings/components/Action"; -import { useActiveElement } from "~/editor/hooks/useActiveElement"; -import useElementSettings from "~/editor/plugins/elementSettings/hooks/useElementSettings"; -import { useRefreshBlock } from "~/editor/hooks/useRefreshBlock"; -import VariableSettings from "~/editor/plugins/elementSettings/variable/VariableSettings"; -import { useTemplateMode } from "~/pageEditor/hooks/useTemplateMode"; -import { PbEditorElement } from "~/types"; - -export const ElementSettingsTabContentPlugin = createDecorator( - SidebarActions, - SidebarActionsWrapper => { - return function SettingsTabContent({ children, ...props }) { - const [element] = useActiveElement(); - const elementSettings = useElementSettings(); - const [isTemplateMode] = useTemplateMode(); - const { refreshBlock, loading } = useRefreshBlock(element as PbEditorElement); - - if (isTemplateMode) { - return <VariableSettings />; - } - - const isReferenceBlockElement = element?.data?.blockId; - - return ( - <> - <SidebarActionsWrapper {...props}> - {isReferenceBlockElement ? ( - <> - {elementSettings.map(({ plugin, options }, index) => { - return ( - <div key={plugin.name + "-" + index}> - {typeof plugin.renderAction === "function" && - plugin.name !== - "pb-editor-page-element-settings-save" && - plugin.renderAction({ options })} - </div> - ); - })} - <Action - tooltip={"Edit block"} - icon={<EditIcon />} - onClick={() => - window.open( - `/page-builder/block-editor/${element?.data?.blockId}`, - "_blank", - "noopener" - ) - } - /> - <Action - disabled={loading} - tooltip={loading ? "Refreshing..." : "Refresh block"} - onClick={refreshBlock} - icon={<RefreshIcon />} - /> - </> - ) : ( - children - )} - </SidebarActionsWrapper> - {isReferenceBlockElement && <VariableSettings />} - </> - ); - }; - } -); diff --git a/packages/app-page-builder/src/pageEditor/config/PageEditorConfig.tsx b/packages/app-page-builder/src/pageEditor/config/PageEditorConfig.tsx deleted file mode 100644 index 61df984a7e3..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/PageEditorConfig.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import { EventActionPlugins, EventActionHandlerPlugin } from "./eventActions"; -import { EditorBarPlugins } from "./editorBar"; -import { BlockEditingPlugin } from "./blockEditing"; -import { BlockElementSidebarPlugin } from "./BlockElementSidebarPlugin"; -import { ElementSettingsTabContentPlugin } from "./ElementSettingsTabContentPlugin"; -import { ToolbarActionsPlugin } from "./ToolbarActionsPlugin"; - -export const PageEditorConfig = React.memo(() => { - return ( - <> - <EventActionHandlerPlugin /> - <EditorBarPlugins /> - <EventActionPlugins /> - <BlockEditingPlugin /> - <BlockElementSidebarPlugin /> - <ElementSettingsTabContentPlugin /> - <ToolbarActionsPlugin /> - </> - ); -}); - -PageEditorConfig.displayName = "PageEditorConfig"; diff --git a/packages/app-page-builder/src/pageEditor/config/Sidebar/TemplateMode.tsx b/packages/app-page-builder/src/pageEditor/config/Sidebar/TemplateMode.tsx new file mode 100644 index 00000000000..a4fe9a2a424 --- /dev/null +++ b/packages/app-page-builder/src/pageEditor/config/Sidebar/TemplateMode.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { VariableSettings } from "~/editor/plugins/elementSettings/variable/VariableSettings"; +import { useTemplateMode } from "~/pageEditor/hooks/useTemplateMode"; +import { PageEditorConfig } from "~/pageEditor/editorConfig/PageEditorConfig"; + +export const TemplateMode = PageEditorConfig.Sidebar.Elements.createDecorator(Original => { + return function TemplateMode(props) { + const [isTemplateMode] = useTemplateMode(); + + if (props.group === "groups" && isTemplateMode) { + return <VariableSettings />; + } + + return <Original {...props} />; + }; +}); diff --git a/packages/app-page-builder/src/pageEditor/config/Sidebar/UnlinkBlock.tsx b/packages/app-page-builder/src/pageEditor/config/Sidebar/UnlinkBlock.tsx new file mode 100644 index 00000000000..d20d9d2806a --- /dev/null +++ b/packages/app-page-builder/src/pageEditor/config/Sidebar/UnlinkBlock.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import styled from "@emotion/styled"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; +import { ButtonPrimary } from "@webiny/ui/Button"; +import UnlinkBlockAction from "~/pageEditor/plugins/elementSettings/UnlinkBlockAction"; +import { ReactComponent as InfoIcon } from "@webiny/app-admin/assets/icons/info.svg"; +import { useTemplateMode } from "~/pageEditor/hooks/useTemplateMode"; +import { PageEditorConfig } from "~/pageEditor/editorConfig/PageEditorConfig"; +import { useBlockReference } from "~/templateEditor/config/Sidebar/useBlockReference"; + +type UnlinkBlockWrapperProps = { + permission: boolean; +}; + +const UnlinkBlockWrapper = styled("div")<UnlinkBlockWrapperProps>` + padding: 16px; + display: grid; + row-gap: 16px; + justify-content: center; + align-items: center; + margin: auto 16px 16px 16px; + text-align: center; + background-color: var(--mdc-theme-background); + border: 3px dashed var(--webiny-theme-color-border); + border-radius: 5px; + opacity: ${props => !props.permission && "0.5"}; + + & .button-wrapper { + font-weight: bold; + } + + & .info-wrapper { + display: flex; + align-items: center; + font-size: 10px; + + & svg { + width: 18px; + margin-right: 5px; + } + } +`; + +type UnlinkTabProps = { + permission: boolean; +}; + +const UnlinkWidget = ({ permission }: UnlinkTabProps) => { + return ( + <UnlinkBlockWrapper permission={permission}> + This is a block element - to change it you need to unlink it first. By unlinking it, any + changes made to the block will no longer automatically reflect on this page. + <div className="button-wrapper"> + {permission ? ( + <UnlinkBlockAction> + <ButtonPrimary>Unlink block</ButtonPrimary> + </UnlinkBlockAction> + ) : ( + "No permissions" + )} + </div> + <div className="info-wrapper"> + <InfoIcon /> Click here to learn more about how blocks work. + </div> + </UnlinkBlockWrapper> + ); +}; + +/** + * The purpose of this component is to intercept the "style" elements group, and do one of the following: + * - if in "template mode", hide the "style" tab + * - render the "Unlink Block" widget, if the block is "linked" + */ +export const UnlinkBlock = PageEditorConfig.Sidebar.Elements.createDecorator(Original => { + return function SidebarElements(props) { + const [element] = useActiveElement(); + const blockReference = useBlockReference(); + const [isTemplateMode] = useTemplateMode(); + + if (!element || props.group !== "style") { + return <Original {...props} />; + } + + if (isTemplateMode) { + return null; + } + + if (blockReference) { + return <UnlinkWidget permission={true} />; + } + + return <Original {...props} />; + }; +}); diff --git a/packages/app-page-builder/src/pageEditor/config/Sidebar/VariableSettings.tsx b/packages/app-page-builder/src/pageEditor/config/Sidebar/VariableSettings.tsx new file mode 100644 index 00000000000..bc7c7513c8a --- /dev/null +++ b/packages/app-page-builder/src/pageEditor/config/Sidebar/VariableSettings.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { VariableSettings as BaseVariableSettings } from "~/editor/plugins/elementSettings/variable/VariableSettings"; +import { useTemplateMode } from "~/pageEditor/hooks/useTemplateMode"; +import { useBlockReference } from "~/templateEditor/config/Sidebar/useBlockReference"; + +export const VariableSettings = () => { + const blockReference = useBlockReference(); + const [isTemplateMode] = useTemplateMode(); + + if (isTemplateMode || blockReference) { + return <BaseVariableSettings />; + } + + return null; +}; diff --git a/packages/app-page-builder/src/pageEditor/config/Toolbar/UnlinkTemplate.tsx b/packages/app-page-builder/src/pageEditor/config/Toolbar/UnlinkTemplate.tsx new file mode 100644 index 00000000000..299938d673f --- /dev/null +++ b/packages/app-page-builder/src/pageEditor/config/Toolbar/UnlinkTemplate.tsx @@ -0,0 +1,97 @@ +import React, { useCallback, useState } from "react"; +import { css } from "emotion"; +import { useMutation } from "@apollo/react-hooks"; +import { IconButton, ButtonPrimary } from "@webiny/ui/Button"; +import { Dialog, DialogCancel, DialogTitle, DialogActions, DialogContent } from "@webiny/ui/Dialog"; +import { ReactComponent as LockIcon } from "@material-design-icons/svg/outlined/lock.svg"; +import { useTemplateMode } from "~/pageEditor/hooks/useTemplateMode"; +import { usePage } from "~/pageEditor/hooks/usePage"; +import { UNLINK_PAGE_FROM_TEMPLATE } from "~/pageEditor/graphql"; +import { PageEditorConfig } from "~/pageEditor/editorConfig/PageEditorConfig"; + +const unlinkTemplateDialog = css` + & .mdc-dialog__surface { + width: 500px; + } + + & .webiny-ui-dialog__title { + text-transform: uppercase; + } + + & p { + margin-bottom: 16px; + } + + & .info-wrapper { + display: flex; + align-items: center; + font-size: 12px; + + & svg { + width: 18px; + margin-right: 5px; + } + } +`; + +const UnlinkTemplateAction = () => { + const [isModalShown, setIsModalShown] = useState(false); + const [unlinkPage, unlinkPageMutation] = useMutation(UNLINK_PAGE_FROM_TEMPLATE); + + const [page] = usePage(); + + const onOpen = useCallback(() => { + setIsModalShown(true); + }, []); + + const onClose = useCallback(() => { + setIsModalShown(false); + }, []); + + const onUnlink = useCallback(() => { + unlinkPage({ + variables: { id: page.id } + }).then(() => { + // TODO: We do a screen refresh just because of some weird state inconsistency-related + // TODO: issue. Should fix this when there's more time at hand. + window.location.reload(); + }); + }, [page.id]); + + return ( + <> + <IconButton icon={<LockIcon />} onClick={onOpen} /> + <Dialog open={isModalShown} onClose={onClose} className={unlinkTemplateDialog}> + <DialogTitle>Unlink Template</DialogTitle> + <DialogContent> + <p> + This page was created from a template. To change it, you need to unlink it + first. + </p> + <p> + By unlinking it, any changes made to the template will no longer be + reflected on this page. + </p> + </DialogContent> + <DialogActions> + <DialogCancel onClick={onClose}>Cancel</DialogCancel> + <ButtonPrimary disabled={unlinkPageMutation.loading} onClick={onUnlink}> + Unlink template + </ButtonPrimary> + </DialogActions> + </Dialog> + </> + ); +}; + +export const UnlinkTemplate = PageEditorConfig.Toolbar.Elements.createDecorator(Original => { + return function UnlinkTemplate(props) { + const [isTemplateMode] = useTemplateMode(); + + if (props.group === "top" && isTemplateMode) { + return <UnlinkTemplateAction />; + } + + return <Original {...props} />; + }; +}); diff --git a/packages/app-page-builder/src/pageEditor/config/ToolbarActionsPlugin.tsx b/packages/app-page-builder/src/pageEditor/config/ToolbarActionsPlugin.tsx deleted file mode 100644 index 59b30f23ff4..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/ToolbarActionsPlugin.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React, { useCallback, useState } from "react"; -import { css } from "emotion"; -import { useRecoilValue } from "recoil"; - -import { createDecorator } from "@webiny/app-admin"; -import { plugins } from "@webiny/plugins"; -import { IconButton, ButtonPrimary } from "@webiny/ui/Button"; -import { Dialog, DialogCancel, DialogTitle, DialogActions, DialogContent } from "@webiny/ui/Dialog"; -import { ReactComponent as LockIcon } from "@material-design-icons/svg/outlined/lock.svg"; -import { ToolbarActions } from "~/editor"; -import { renderPlugin } from "~/editor/components/Editor/Toolbar"; -import { useTemplateMode } from "~/pageEditor/hooks/useTemplateMode"; -import { rootElementAtom, elementByIdSelector } from "~/editor/recoil/modules"; -import { PbEditorElement, PbEditorToolbarBottomPlugin, PbEditorToolbarTopPlugin } from "~/types"; -import { useMutation } from "@apollo/react-hooks"; -import { usePage } from "~/pageEditor"; -import { UNLINK_PAGE_FROM_TEMPLATE } from "~/pageEditor/graphql"; - -const unlinkTemplateDialog = css` - & .mdc-dialog__surface { - width: 500px; - } - - & .webiny-ui-dialog__title { - text-transform: uppercase; - } - - & p { - margin-bottom: 16px; - } - - & .info-wrapper { - display: flex; - align-items: center; - font-size: 12px; - - & svg { - width: 18px; - margin-right: 5px; - } - } -`; - -export const ToolbarActionsPlugin = createDecorator(ToolbarActions, ToolbarActionsWrapper => { - return function BlockEditorToolbarActions() { - const actionsTop = plugins.byType<PbEditorToolbarTopPlugin>("pb-editor-toolbar-top"); - const actionsBottom = plugins.byType<PbEditorToolbarBottomPlugin>( - "pb-editor-toolbar-bottom" - ); - const [isTemplateMode] = useTemplateMode(); - const [isModalShown, setIsModalShown] = useState(false); - const rootElementId = useRecoilValue(rootElementAtom); - const rootElement = useRecoilValue(elementByIdSelector(rootElementId)) as PbEditorElement; - const [unlinkPage, unlinkPageMutation] = useMutation(UNLINK_PAGE_FROM_TEMPLATE); - - const [page] = usePage(); - - // TODO: check if the below check even works. - const unlinkPermission = true; - // const unlinkPermission = useMemo((): boolean => { - // const permission = getPermission<PageBuilderSecurityPermission>("pb.template.unlink"); - // if (permission?.name === "*" || permission?.name === "pb.*") { - // return true; - // } - // return Boolean(permission); - // }, [identity]); - - const onOpen = useCallback(() => { - setIsModalShown(true); - }, []); - - const onClose = useCallback(() => { - setIsModalShown(false); - }, []); - - const onUnlink = useCallback(() => { - unlinkPage({ - variables: { id: page.id } - }).then(() => { - // TODO: We do a screen refresh just because of some weird state inconsistency-related - // TODO: issue. Should fix this when there's more time at hand. - window.location.reload(); - }); - }, [rootElement]); - - return ( - <> - <ToolbarActionsWrapper> - {isTemplateMode ? ( - <IconButton icon={<LockIcon />} onClick={onOpen} /> - ) : ( - <div>{actionsTop.map(renderPlugin)}</div> - )} - <div>{actionsBottom.map(renderPlugin)}</div> - </ToolbarActionsWrapper> - <Dialog open={isModalShown} onClose={onClose} className={unlinkTemplateDialog}> - <DialogTitle>Unlink Template</DialogTitle> - <DialogContent> - <p> - This page was created from a template. To change it, you need to unlink - it first. - </p> - <p> - By unlinking it, any changes made to the template will no longer be - reflected on this page. - </p> - {/* TODO: Bring back when there's actually a link to the docs. */} - {/*<div className="info-wrapper">*/} - {/* <InfoIcon /> Click here to learn more about how page templates work*/} - {/*</div>*/} - </DialogContent> - <DialogActions> - <DialogCancel onClick={onClose}>Cancel</DialogCancel> - <ButtonPrimary - disabled={!unlinkPermission || unlinkPageMutation.loading} - onClick={onUnlink} - > - {unlinkPermission ? "Unlink template" : "No permissions"} - </ButtonPrimary> - </DialogActions> - </Dialog> - </> - ); - }; -}); diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/BackButton/BackButton.tsx b/packages/app-page-builder/src/pageEditor/config/TopBar/BackButton/BackButton.tsx similarity index 55% rename from packages/app-page-builder/src/pageEditor/config/editorBar/BackButton/BackButton.tsx rename to packages/app-page-builder/src/pageEditor/config/TopBar/BackButton/BackButton.tsx index 71406fbd73c..1cd93ddcf81 100644 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/BackButton/BackButton.tsx +++ b/packages/app-page-builder/src/pageEditor/config/TopBar/BackButton/BackButton.tsx @@ -1,26 +1,26 @@ import React from "react"; -import { createDecorator } from "@webiny/app-admin"; -import { ReactComponent as BackIcon } from "./round-arrow_back-24px.svg"; +import { ReactComponent as BackIcon } from "@material-design-icons/svg/round/arrow_back.svg"; import { css } from "emotion"; import { IconButton } from "@webiny/ui/Button"; -import { EditorBar } from "~/editor"; import { useNavigatePage } from "~/admin/hooks/useNavigatePage"; +import { TopBar } from "~/editor/config/TopBar/TopBar"; const backStyles = css({ marginLeft: -10 }); -export const BackButtonPlugin = createDecorator(EditorBar.BackButton, () => { - return function BackButton() { - const navigate = useNavigatePage(); +export const BackButton = () => { + const navigate = useNavigatePage(); - return ( + return ( + <> <IconButton data-testid="pb-editor-back-button" className={backStyles} onClick={navigate.navigateToLatestFolder} icon={<BackIcon />} /> - ); - }; -}); + <TopBar.Divider /> + </> + ); +}; diff --git a/packages/app-page-builder/src/pageEditor/config/TopBar/PageOptionsDropdown/PageOptionsDropdown.tsx b/packages/app-page-builder/src/pageEditor/config/TopBar/PageOptionsDropdown/PageOptionsDropdown.tsx new file mode 100644 index 00000000000..c62e14eb524 --- /dev/null +++ b/packages/app-page-builder/src/pageEditor/config/TopBar/PageOptionsDropdown/PageOptionsDropdown.tsx @@ -0,0 +1,27 @@ +import React, { Fragment } from "react"; +import { css } from "emotion"; +import { Menu } from "@webiny/ui/Menu"; +import { IconButton } from "@webiny/ui/Button"; +import { ReactComponent as MoreVerticalIcon } from "@material-design-icons/svg/round/more_vert.svg"; +import { TopBar } from "~/editor/config/TopBar/TopBar"; + +const menuStyles = css` + .disabled { + opacity: 0.5; + pointer-events: none; + } +`; + +export const PageOptionsDropdown = () => { + return ( + <Menu + data-testid="pb-editor-page-options-menu" + className={menuStyles} + handle={<IconButton icon={<MoreVerticalIcon />} />} + > + {/* We need to have more than 1 element in the children to force the Menu to render as a regular Menu. */} + <Fragment /> + <TopBar.Elements group={"dropdownActions"} /> + </Menu> + ); +}; diff --git a/packages/app-page-builder/src/pageEditor/config/TopBar/PageSettings/PageSettings.tsx b/packages/app-page-builder/src/pageEditor/config/TopBar/PageSettings/PageSettings.tsx new file mode 100644 index 00000000000..ebdc20574f7 --- /dev/null +++ b/packages/app-page-builder/src/pageEditor/config/TopBar/PageSettings/PageSettings.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { UIViewComponent } from "@webiny/app-admin/ui/UIView"; + +/* For the time being, we're importing from the base editor, to not break things for existing users. */ +import { PageSettingsView } from "~/editor/ui/views/PageSettingsView"; + +import { usePageSettings } from "./usePageSettings"; + +export const PageSettingsOverlay = () => { + const { isOpen } = usePageSettings(); + + return isOpen ? <UIViewComponent view={new PageSettingsView()} /> : null; +}; diff --git a/packages/app-page-builder/src/pageEditor/config/TopBar/PageSettings/PageSettingsButton.tsx b/packages/app-page-builder/src/pageEditor/config/TopBar/PageSettings/PageSettingsButton.tsx new file mode 100644 index 00000000000..f4b14510d0a --- /dev/null +++ b/packages/app-page-builder/src/pageEditor/config/TopBar/PageSettings/PageSettingsButton.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { IconButton } from "@webiny/ui/Button"; +import { ReactComponent as SettingsIcon } from "@material-design-icons/svg/round/settings.svg"; +import { usePageSettings } from "./usePageSettings"; + +export const PageSettingsButton = () => { + const { openSettings } = usePageSettings(); + + return <IconButton onClick={openSettings} icon={<SettingsIcon />} />; +}; diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/PageSettingsStyled.tsx b/packages/app-page-builder/src/pageEditor/config/TopBar/PageSettings/PageSettingsStyled.tsx similarity index 100% rename from packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/PageSettingsStyled.tsx rename to packages/app-page-builder/src/pageEditor/config/TopBar/PageSettings/PageSettingsStyled.tsx diff --git a/packages/app-page-builder/src/pageEditor/config/TopBar/PageSettings/usePageSettings.ts b/packages/app-page-builder/src/pageEditor/config/TopBar/PageSettings/usePageSettings.ts new file mode 100644 index 00000000000..85cb9ee3478 --- /dev/null +++ b/packages/app-page-builder/src/pageEditor/config/TopBar/PageSettings/usePageSettings.ts @@ -0,0 +1,23 @@ +import { useCallback } from "react"; +import { atom, useRecoilState } from "recoil"; + +export type PageSettingsState = boolean; + +export const pageSettingsStateAtom = atom<PageSettingsState>({ + key: "pageSettingsStateAtom", + default: false +}); + +export function usePageSettings() { + const [isOpen, setOpen] = useRecoilState(pageSettingsStateAtom); + + const openSettings = useCallback(() => { + setOpen(true); + }, []); + + const closeSettings = useCallback(() => { + setOpen(false); + }, []); + + return { isOpen, openSettings, closeSettings }; +} diff --git a/packages/app-page-builder/src/pageEditor/config/TopBar/PreviewPageOption/PreviewPageOption.tsx b/packages/app-page-builder/src/pageEditor/config/TopBar/PreviewPageOption/PreviewPageOption.tsx new file mode 100644 index 00000000000..10d72e12277 --- /dev/null +++ b/packages/app-page-builder/src/pageEditor/config/TopBar/PreviewPageOption/PreviewPageOption.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { ReactComponent as PreviewIcon } from "@material-design-icons/svg/round/visibility.svg"; +import { usePage } from "~/pageEditor/hooks/usePage"; +import { usePreviewPage } from "~/admin/hooks/usePreviewPage"; +import { PageEditorConfig } from "~/pageEditor/editorConfig/PageEditorConfig"; + +const { TopBar } = PageEditorConfig; + +export const PreviewPageOption = () => { + const [page] = usePage(); + const { previewPage } = usePreviewPage({ + id: page.id, + status: page.status, + path: page.path + }); + + return ( + <TopBar.DropdownAction.MenuItem + label={"Preview"} + onClick={previewPage} + icon={<PreviewIcon />} + data-testid={"pb-editor-page-options-menu-preview"} + /> + ); +}; diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PublishPageButton/PublishPageButton.tsx b/packages/app-page-builder/src/pageEditor/config/TopBar/PublishPageButton/PublishPageButton.tsx similarity index 84% rename from packages/app-page-builder/src/pageEditor/config/editorBar/PublishPageButton/PublishPageButton.tsx rename to packages/app-page-builder/src/pageEditor/config/TopBar/PublishPageButton/PublishPageButton.tsx index c928d1b910b..a56b558f8c8 100644 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/PublishPageButton/PublishPageButton.tsx +++ b/packages/app-page-builder/src/pageEditor/config/TopBar/PublishPageButton/PublishPageButton.tsx @@ -5,8 +5,7 @@ import { ConfirmationDialog } from "@webiny/ui/ConfirmationDialog"; import { ButtonPrimary } from "@webiny/ui/Button"; import { usePagesPermissions } from "~/hooks/permissions"; import { useAdminPageBuilder } from "~/admin/hooks/useAdminPageBuilder"; -import { createDecorator, makeDecoratable } from "@webiny/app-admin"; -import { EditorBar } from "~/editor"; +import { makeDecoratable } from "@webiny/app-admin"; import { usePage } from "~/pageEditor/hooks/usePage"; import { useNavigatePage } from "~/admin/hooks/useNavigatePage"; @@ -66,14 +65,3 @@ const DefaultPublishPageButton = () => { }; export const PublishPageButton = makeDecoratable("PublishPageButton", DefaultPublishPageButton); - -export const PublishPageButtonPlugin = createDecorator(EditorBar.RightSection, RightSection => { - return function AddPublishPageButton(props) { - return ( - <RightSection> - <PublishPageButton /> - {props.children} - </RightSection> - ); - }; -}); diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PublishPageButton/graphql.ts b/packages/app-page-builder/src/pageEditor/config/TopBar/PublishPageButton/graphql.ts similarity index 100% rename from packages/app-page-builder/src/pageEditor/config/editorBar/PublishPageButton/graphql.ts rename to packages/app-page-builder/src/pageEditor/config/TopBar/PublishPageButton/graphql.ts diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/Revisions/Revisions.tsx b/packages/app-page-builder/src/pageEditor/config/TopBar/Revisions/Revisions.tsx similarity index 77% rename from packages/app-page-builder/src/pageEditor/config/editorBar/Revisions/Revisions.tsx rename to packages/app-page-builder/src/pageEditor/config/TopBar/Revisions/Revisions.tsx index ca18e6260ca..e5e48c5b78b 100644 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/Revisions/Revisions.tsx +++ b/packages/app-page-builder/src/pageEditor/config/TopBar/Revisions/Revisions.tsx @@ -1,14 +1,12 @@ import React from "react"; import { css } from "emotion"; +import { ReactComponent as DownButton } from "@material-design-icons/svg/round/arrow_drop_down.svg"; import { Menu, MenuItem } from "@webiny/ui/Menu"; import { ButtonDefault } from "@webiny/ui/Button"; import { Icon } from "@webiny/ui/Icon"; import { Typography } from "@webiny/ui/Typography"; -import { ReactComponent as DownButton } from "./round-arrow_drop_down-24px.svg"; import { useRevisions } from "~/pageEditor/hooks/useRevisions"; import { RevisionItemAtomType } from "~/pageEditor/state"; -import { createDecorator } from "@webiny/app-admin"; -import { EditorBar } from "~/editor"; import { useNavigatePage } from "~/admin/hooks/useNavigatePage"; const buttonStyle = css({ @@ -40,7 +38,7 @@ const getStatus = (revision: RevisionItemAtomType): RevisionStatusEnum => { return RevisionStatusEnum.DRAFT; }; -const Revisions = () => { +export const RevisionsDropdownMenu = () => { const [revisions] = useRevisions(); const { navigateToPageEditor } = useNavigatePage(); @@ -68,19 +66,3 @@ const Revisions = () => { </Menu> ); }; - -const ComposeRevisionSelector = createDecorator(EditorBar.RightSection, RightSection => { - return function ComposeRightSection(props) { - return ( - <RightSection> - <Revisions /> - <EditorBar.Divider /> - {props.children} - </RightSection> - ); - }; -}); - -export const RevisionsPlugin = () => { - return <ComposeRevisionSelector />; -}; diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PageOptionsMenu/SetAsHomepageButton.tsx b/packages/app-page-builder/src/pageEditor/config/TopBar/SetAsHomepageOption/SetAsHomepageOption.tsx similarity index 59% rename from packages/app-page-builder/src/pageEditor/config/editorBar/PageOptionsMenu/SetAsHomepageButton.tsx rename to packages/app-page-builder/src/pageEditor/config/TopBar/SetAsHomepageOption/SetAsHomepageOption.tsx index da0de7436a5..9ca4f1de8cf 100644 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/PageOptionsMenu/SetAsHomepageButton.tsx +++ b/packages/app-page-builder/src/pageEditor/config/TopBar/SetAsHomepageOption/SetAsHomepageOption.tsx @@ -1,24 +1,34 @@ import React, { useCallback } from "react"; +import { ReactComponent as HomeIcon } from "@material-design-icons/svg/round/home.svg"; +import { useConfirmationDialog } from "@webiny/app-admin"; import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar"; -import { ConfirmationDialog } from "@webiny/ui/ConfirmationDialog"; -import { MenuItem } from "@webiny/ui/Menu"; -import { ListItemGraphic } from "@webiny/ui/List"; -import { Icon } from "@webiny/ui/Icon"; -import { ReactComponent as HomeIcon } from "~/admin/assets/round-home-24px.svg"; -import { usePageBuilderSettings } from "~/admin/hooks/usePageBuilderSettings"; import { useAdminPageBuilder } from "~/admin/hooks/useAdminPageBuilder"; import { usePage } from "~/pageEditor/hooks/usePage"; import { useNavigatePage } from "~/admin/hooks/useNavigatePage"; +import { usePageBuilderSettings } from "~/admin/hooks/usePageBuilderSettings"; +import { PageEditorConfig } from "~/pageEditor/editorConfig/PageEditorConfig"; + +const { TopBar } = PageEditorConfig; -export const SetAsHomepageButton = React.memo(() => { +export const SetAsHomepageOption = () => { const [page] = usePage(); const { navigateToLatestFolder } = useNavigatePage(); const { showSnackbar } = useSnackbar(); const pageBuilder = useAdminPageBuilder(); + const { showConfirmation } = useConfirmationDialog({ + message: ( + <span> + You're about to set this page as your new homepage, are you sure you want to + continue? + <br /> + Note that the page will automatically be published. + </span> + ) + }); const { settings, updateSettingsMutation, isSpecialPage } = usePageBuilderSettings(); - const setAsHomepage = useCallback(async () => { + const setPageAsHomepage = useCallback(async () => { const publishPageResult = await pageBuilder.publishPage(page as { id: string }, { client: pageBuilder.client }); @@ -29,6 +39,7 @@ export const SetAsHomepageButton = React.memo(() => { if (!publishPageResult) { return; } + if (publishPageResult.error) { return showSnackbar(publishPageResult.error.message); } @@ -57,29 +68,11 @@ export const SetAsHomepageButton = React.memo(() => { }, [page.id]); return ( - <ConfirmationDialog - message={ - <span> - You're about to set this page as your new homepage, are you sure you want to - continue? - <br /> - Note that the page will automatically be published. - </span> - } - > - {({ showConfirmation }) => ( - <MenuItem - disabled={isSpecialPage(page.pid!, "home")} - onClick={() => showConfirmation(setAsHomepage)} - > - <ListItemGraphic> - <Icon icon={<HomeIcon />} /> - </ListItemGraphic> - Set as homepage - </MenuItem> - )} - </ConfirmationDialog> + <TopBar.DropdownAction.MenuItem + label={"Set as homepage"} + onClick={() => showConfirmation(setPageAsHomepage)} + icon={<HomeIcon />} + disabled={isSpecialPage(page.pid!, "home")} + /> ); -}); - -SetAsHomepageButton.displayName = "SetAsHomepageButton"; +}; diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/Title/Styled.ts b/packages/app-page-builder/src/pageEditor/config/TopBar/Title/Styled.ts similarity index 100% rename from packages/app-page-builder/src/pageEditor/config/editorBar/Title/Styled.ts rename to packages/app-page-builder/src/pageEditor/config/TopBar/Title/Styled.ts diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/Title/Title.tsx b/packages/app-page-builder/src/pageEditor/config/TopBar/Title/Title.tsx similarity index 91% rename from packages/app-page-builder/src/pageEditor/config/editorBar/Title/Title.tsx rename to packages/app-page-builder/src/pageEditor/config/TopBar/Title/Title.tsx index a2411f3ee8d..c7b9fc3c1d4 100644 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/Title/Title.tsx +++ b/packages/app-page-builder/src/pageEditor/config/TopBar/Title/Title.tsx @@ -3,7 +3,6 @@ import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar"; import { Input } from "@webiny/ui/Input"; import { Tooltip } from "@webiny/ui/Tooltip"; import { Typography } from "@webiny/ui/Typography"; -import { createDecorator } from "@webiny/app-admin"; import { PageMeta, PageTitle, @@ -16,7 +15,6 @@ import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; import { usePage } from "~/pageEditor/hooks/usePage"; import { PageAtomType } from "~/pageEditor/state"; import { UpdateDocumentActionEvent } from "~/editor/recoil/actions"; -import { EditorBar } from "~/editor"; declare global { interface Window { @@ -43,7 +41,7 @@ const extractPageInfo = (page: PageAtomType): PageInfo => { }; }; -const Title = () => { +export const Title = () => { const handler = useEventActionHandler(); const [page] = usePage(); const { showSnackbar } = useSnackbar(); @@ -108,7 +106,7 @@ const Title = () => { const autoFocus = !window.Cypress; return editTitle ? ( - <TitleInputWrapper> + <TitleInputWrapper data-testid="pb-editor-page-title"> <Input autoFocus={autoFocus} fullwidth @@ -140,14 +138,3 @@ const Title = () => { </TitleWrapper> ); }; - -export const TitlePlugin = createDecorator(EditorBar.LeftSection, LeftSection => { - return function AddTitle(props) { - return ( - <LeftSection> - {props.children} - <Title /> - </LeftSection> - ); - }; -}); diff --git a/packages/app-page-builder/src/pageEditor/config/blockEditing/AddBlock.tsx b/packages/app-page-builder/src/pageEditor/config/blockEditing/AddBlock.tsx deleted file mode 100644 index 6d66504cd0c..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/blockEditing/AddBlock.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import styled from "@emotion/styled"; -import { ButtonFloating } from "@webiny/ui/Button"; -import { ReactComponent as AddIcon } from "~/editor/assets/icons/add.svg"; -import { blocksBrowserStateAtom } from "~/pageEditor/config/blockEditing/state"; -import { useRecoilState } from "recoil"; - -const SIDEBAR_WIDTH = 300; -const BottomRight = styled("div")({ - position: "fixed", - zIndex: 25, - bottom: 20, - right: 20 + SIDEBAR_WIDTH -}); - -const AddBlock = () => { - const [, setBlocksBrowserState] = useRecoilState(blocksBrowserStateAtom); - - const onClickHandler = () => { - setBlocksBrowserState(true); - }; - - return ( - <BottomRight> - <ButtonFloating onClick={onClickHandler} icon={<AddIcon />} /> - </BottomRight> - ); -}; - -export default React.memo(AddBlock); diff --git a/packages/app-page-builder/src/pageEditor/config/blockEditing/icons/delete-24dpx.svg b/packages/app-page-builder/src/pageEditor/config/blockEditing/icons/delete-24dpx.svg deleted file mode 100644 index ebce1505c1b..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/blockEditing/icons/delete-24dpx.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px" height="24px"> - <path d="M0 0h24v24H0V0z" fill="none"/> - <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2H8c-1.1 0-2 .9-2 2v10zM18 4h-2.5l-.71-.71c-.18-.18-.44-.29-.7-.29H9.91c-.26 0-.52.11-.7.29L8.5 4H6c-.55 0-1 .45-1 1s.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1z" fill="currentColor"/> -</svg> \ No newline at end of file diff --git a/packages/app-page-builder/src/pageEditor/config/blockEditing/icons/round-clear_all-24px.svg b/packages/app-page-builder/src/pageEditor/config/blockEditing/icons/round-clear_all-24px.svg deleted file mode 100644 index 28bd3840c63..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/blockEditing/icons/round-clear_all-24px.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <path fill="none" d="M0 0h24v24H0V0z"/> - <path fill="currentColor" d="M6 13h12c.55 0 1-.45 1-1s-.45-1-1-1H6c-.55 0-1 .45-1 1s.45 1 1 1zm-2 4h12c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1s.45 1 1 1zm3-9c0 .55.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1H8c-.55 0-1 .45-1 1z"/></svg> \ No newline at end of file diff --git a/packages/app-page-builder/src/pageEditor/config/blockEditing/icons/round-edit-24px.svg b/packages/app-page-builder/src/pageEditor/config/blockEditing/icons/round-edit-24px.svg deleted file mode 100644 index 77ebdee629d..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/blockEditing/icons/round-edit-24px.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> - <path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/> - <path d="M0 0h24v24H0z" fill="none"/> -</svg> \ No newline at end of file diff --git a/packages/app-page-builder/src/pageEditor/config/blockEditing/index.tsx b/packages/app-page-builder/src/pageEditor/config/blockEditing/index.tsx deleted file mode 100644 index cc59e9bee40..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/blockEditing/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from "react"; -import { createDecorator } from "@webiny/app-admin"; -import { useRecoilValue } from "recoil"; -import AddBlock from "./AddBlock"; -import AddContent from "./AddContent"; -import SearchBlocks from "./SearchBlocks"; -import { EditorBar, EditorContent as BaseEditorContent } from "~/editor"; -import { blocksBrowserStateAtom } from "~/pageEditor/config/blockEditing/state"; -import { useTemplateMode } from "~/pageEditor/hooks/useTemplateMode"; - -const BlockBrowser = createDecorator(EditorBar, EditorBar => { - return function PageSettingsOverlay() { - const isActive = useRecoilValue(blocksBrowserStateAtom); - - return ( - <> - <EditorBar /> - {isActive ? <SearchBlocks /> : null} - </> - ); - }; -}); - -const EditorContent = createDecorator(BaseEditorContent, PrevContent => { - return function EditorContent() { - const [isTemplateMode] = useTemplateMode(); - - return ( - <> - <PrevContent /> - {!isTemplateMode && ( - <> - <AddBlock /> - <AddContent /> - </> - )} - </> - ); - }; -}); - -export const BlockEditingPlugin = () => { - return ( - <> - <BlockBrowser /> - <EditorContent /> - </> - ); -}; diff --git a/packages/app-page-builder/src/pageEditor/config/blockEditing/state.ts b/packages/app-page-builder/src/pageEditor/config/blockEditing/state.ts deleted file mode 100644 index fc8783353d4..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/blockEditing/state.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { atom } from "recoil"; - -export type BlocksBrowserState = boolean; - -export const blocksBrowserStateAtom = atom<BlocksBrowserState>({ - key: "blocksBrowserStateAtom", - default: false -}); diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/BackButton/index.ts b/packages/app-page-builder/src/pageEditor/config/editorBar/BackButton/index.ts deleted file mode 100644 index 26e35db76f4..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/BackButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./BackButton"; diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/BackButton/round-arrow_back-24px.svg b/packages/app-page-builder/src/pageEditor/config/editorBar/BackButton/round-arrow_back-24px.svg deleted file mode 100644 index d03b90e14b8..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/BackButton/round-arrow_back-24px.svg +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve"> -<g id="Bounding_Boxes"> - <path fill="none" d="M0,0h24v24H0V0z"/> -</g> - <g id="Rounded"> - <path fill="currentColor" d="M19,11H7.83l4.88-4.88c0.39-0.39,0.39-1.03,0-1.42l0,0c-0.39-0.39-1.02-0.39-1.41,0l-6.59,6.59 - c-0.39,0.39-0.39,1.02,0,1.41l6.59,6.59c0.39,0.39,1.02,0.39,1.41,0l0,0c0.39-0.39,0.39-1.02,0-1.41L7.83,13H19c0.55,0,1-0.45,1-1 - v0C20,11.45,19.55,11,19,11z"/> -</g> -</svg> diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/EditorBarPlugins.tsx b/packages/app-page-builder/src/pageEditor/config/editorBar/EditorBarPlugins.tsx deleted file mode 100644 index 81069214cfe..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/EditorBarPlugins.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; -import { BackButtonPlugin } from "./BackButton"; -import { PageSettingsPlugin } from "./PageSettings"; -import { RevisionsPlugin } from "./Revisions"; -import { PublishPageButtonPlugin } from "./PublishPageButton"; -import { TitlePlugin } from "./Title"; -import { PageOptionsMenuPlugin } from "./PageOptionsMenu/PageOptionsMenuPlugin"; -import { PreviewPageButtonPlugin } from "./PreviewPageButton/PreviewPageButton"; -import { SetAsHomepageButtonPlugin } from "./SetAsHomepageButton/SetAsHomepageButton"; - -export const EditorBarPlugins = () => { - return ( - <> - <BackButtonPlugin /> - <TitlePlugin /> - <RevisionsPlugin /> - <PageSettingsPlugin /> - <PageOptionsMenuPlugin /> - <PublishPageButtonPlugin /> - <PreviewPageButtonPlugin /> - <SetAsHomepageButtonPlugin /> - </> - ); -}; diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PageOptionsMenu/PageOptionsMenu.tsx b/packages/app-page-builder/src/pageEditor/config/editorBar/PageOptionsMenu/PageOptionsMenu.tsx deleted file mode 100644 index 07c495e3c3f..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/PageOptionsMenu/PageOptionsMenu.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { Fragment } from "react"; -import { css } from "emotion"; -import { makeDecoratable } from "@webiny/app-admin"; -import { Menu } from "@webiny/ui/Menu"; -import { IconButton } from "@webiny/ui/Button"; -import { ReactComponent as MoreVerticalIcon } from "~/admin/assets/more_vert.svg"; - -const menuStyles = css({ - ".disabled": { - opacity: 0.5, - pointerEvents: "none" - } -}); - -export interface PageOptionsMenuProps { - items: JSX.Element[]; -} - -export const PageOptionsMenu = makeDecoratable( - "PageOptionsMenu", - ({ items }: PageOptionsMenuProps) => { - if (!items.length) { - return null; - } - - return ( - <Menu - data-testid="pb-editor-page-options-menu" - className={menuStyles} - handle={<IconButton icon={<MoreVerticalIcon />} />} - > - {items.map((item, index) => ( - <Fragment key={index}>{item}</Fragment> - ))} - </Menu> - ); - } -); diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PageOptionsMenu/PageOptionsMenuPlugin.tsx b/packages/app-page-builder/src/pageEditor/config/editorBar/PageOptionsMenu/PageOptionsMenuPlugin.tsx deleted file mode 100644 index f3da869e988..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/PageOptionsMenu/PageOptionsMenuPlugin.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; -import { createDecorator } from "@webiny/react-composition"; -import { EditorBar } from "~/editor"; -import { PageOptionsMenu } from "~/pageEditor"; - -export const PageOptionsMenuPlugin = createDecorator(EditorBar.RightSection, RightSection => { - return function AddRevisionSelector(props) { - return ( - <RightSection> - <PageOptionsMenu items={[]} /> - {props.children} - </RightSection> - ); - }; -}); diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PageOptionsMenu/index.ts b/packages/app-page-builder/src/pageEditor/config/editorBar/PageOptionsMenu/index.ts deleted file mode 100644 index 656135abfa9..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/PageOptionsMenu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./PageOptionsMenu"; diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/PageSettings.tsx b/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/PageSettings.tsx deleted file mode 100644 index d0fadb110fa..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/PageSettings.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react"; -import { createDecorator } from "@webiny/app-admin"; -import { UIViewComponent } from "@webiny/app-admin/ui/UIView"; -import { pageSettingsStateAtom } from "./state"; -import { useRecoilValue } from "recoil"; - -/* For the time being, we're importing from the base editor, to not break things for existing users. */ -import { PageSettingsView } from "~/editor/ui/views/PageSettingsView"; -import { EditorBar } from "~/editor"; - -export const PageSettingsOverlay = createDecorator(EditorBar, EditorBar => { - return function PageSettingsOverlay() { - const isActive = useRecoilValue(pageSettingsStateAtom); - - return ( - <> - <EditorBar /> - {isActive ? <UIViewComponent view={new PageSettingsView()} /> : null} - </> - ); - }; -}); diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/PageSettingsButton.tsx b/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/PageSettingsButton.tsx deleted file mode 100644 index 9feba78d741..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/PageSettingsButton.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { useCallback } from "react"; -import { useRecoilState } from "recoil"; -import { IconButton } from "@webiny/ui/Button"; -import { createDecorator } from "@webiny/app-admin"; -import { ReactComponent as SettingsIcon } from "./settings.svg"; -import { pageSettingsStateAtom } from "~/pageEditor/config/editorBar/PageSettings/state"; -import { EditorBar } from "~/editor"; - -const PageSettingsButton = () => { - const [, setState] = useRecoilState(pageSettingsStateAtom); - const onClickHandler = useCallback(() => { - setState(true); - }, []); - - return <IconButton onClick={onClickHandler} icon={<SettingsIcon />} />; -}; - -export const AddPageSettingsButton = createDecorator(EditorBar.RightSection, RightSection => { - return function ComposeRightSection(props) { - return ( - <RightSection> - <PageSettingsButton /> - {props.children} - </RightSection> - ); - }; -}); diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/index.tsx b/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/index.tsx deleted file mode 100644 index ac61c3a9e75..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import { PageSettingsOverlay } from "./PageSettings"; -import { AddPageSettingsButton } from "./PageSettingsButton"; - -export const PageSettingsPlugin = () => { - return ( - <> - <PageSettingsOverlay /> - <AddPageSettingsButton /> - </> - ); -}; diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/settings.svg b/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/settings.svg deleted file mode 100644 index 383b39df409..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/settings.svg +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" - height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve"> -<g id="Header_x2F_BG" display="none"> - <rect x="-402" y="-314" display="inline" fill="#F1F1F2" width="520" height="520"/> -</g> -<g id="Bounding_Boxes"> - <g id="ui_x5F_spec_x5F_header_copy_3" display="none"> - </g> - <path fill="none" d="M0,0h24v24H0V0z"/> -</g> -<g id="Rounded"> - <g id="ui_x5F_spec_x5F_header_copy_5" display="none"> - </g> - <path fill="currentColor" d="M19.43,12.98c0.04-0.32,0.07-0.64,0.07-0.98s-0.03-0.66-0.07-0.98l2.11-1.65c0.19-0.15,0.24-0.42,0.12-0.64l-2-3.46 - c-0.12-0.22-0.39-0.3-0.61-0.22l-2.49,1c-0.52-0.4-1.08-0.73-1.69-0.98l-0.38-2.65C14.46,2.18,14.25,2,14,2h-4 - C9.75,2,9.54,2.18,9.51,2.42L9.13,5.07C8.52,5.32,7.96,5.66,7.44,6.05l-2.49-1c-0.23-0.09-0.49,0-0.61,0.22l-2,3.46 - C2.21,8.95,2.27,9.22,2.46,9.37l2.11,1.65C4.53,11.34,4.5,11.67,4.5,12s0.03,0.66,0.07,0.98l-2.11,1.65 - c-0.19,0.15-0.24,0.42-0.12,0.64l2,3.46c0.12,0.22,0.39,0.3,0.61,0.22l2.49-1c0.52,0.4,1.08,0.73,1.69,0.98l0.38,2.65 - C9.54,21.82,9.75,22,10,22h4c0.25,0,0.46-0.18,0.49-0.42l0.38-2.65c0.61-0.25,1.17-0.59,1.69-0.98l2.49,1 - c0.23,0.09,0.49,0,0.61-0.22l2-3.46c0.12-0.22,0.07-0.49-0.12-0.64L19.43,12.98z M12,15.5c-1.93,0-3.5-1.57-3.5-3.5 - s1.57-3.5,3.5-3.5s3.5,1.57,3.5,3.5S13.93,15.5,12,15.5z"/> -</g> -</svg> diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/state.ts b/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/state.ts deleted file mode 100644 index f0aeb5dd2c0..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/PageSettings/state.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { atom } from "recoil"; - -export type PageSettingsState = boolean; - -export const pageSettingsStateAtom = atom<PageSettingsState>({ - key: "pageSettingsStateAtom", - default: false -}); diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PreviewPageButton/PreviewPageButton.tsx b/packages/app-page-builder/src/pageEditor/config/editorBar/PreviewPageButton/PreviewPageButton.tsx deleted file mode 100644 index e1c6b8176c8..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/PreviewPageButton/PreviewPageButton.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; -import { createDecorator, makeDecoratable } from "@webiny/react-composition"; -import { ListItemGraphic } from "@webiny/ui/List"; -import { Icon } from "@webiny/ui/Icon"; -import { MenuItem } from "@webiny/ui/Menu"; -import { ReactComponent as PreviewIcon } from "~/admin/assets/visibility.svg"; -import { usePage } from "~/pageEditor/hooks/usePage"; -import { PageOptionsMenu } from "~/pageEditor"; -import { usePreviewPage } from "~/admin/hooks/usePreviewPage"; - -export const PreviewPage = makeDecoratable("PreviewPage", () => { - const [page] = usePage(); - const { previewPage } = usePreviewPage({ - id: page.id, - status: page.status, - path: page.path - }); - - return ( - <MenuItem onClick={previewPage} data-testid={"pb-editor-page-options-menu-preview"}> - <ListItemGraphic> - <Icon icon={<PreviewIcon />} /> - </ListItemGraphic> - Preview - </MenuItem> - ); -}); - -export const PreviewPageButtonPlugin = createDecorator(PageOptionsMenu, Original => { - return function PreviewPageButton({ items, ...props }) { - return <Original {...props} items={[<PreviewPage key={"preview"} />, ...items]} />; - }; -}); diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/PublishPageButton/index.ts b/packages/app-page-builder/src/pageEditor/config/editorBar/PublishPageButton/index.ts deleted file mode 100644 index 7efe69ec238..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/PublishPageButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./PublishPageButton"; diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/Revisions/index.ts b/packages/app-page-builder/src/pageEditor/config/editorBar/Revisions/index.ts deleted file mode 100644 index 3150ce27cf3..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/Revisions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./Revisions"; diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/Revisions/round-arrow_drop_down-24px.svg b/packages/app-page-builder/src/pageEditor/config/editorBar/Revisions/round-arrow_drop_down-24px.svg deleted file mode 100644 index d2a91682bb2..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/Revisions/round-arrow_drop_down-24px.svg +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve"> -<g id="Bounding_Boxes"> - <path fill="none" d="M0,0h24v24H0V0z"/> -</g> -<g id="Rounded"> - <path fill="currentColor" d="M8.71,11.71l2.59,2.59c0.39,0.39,1.02,0.39,1.41,0l2.59-2.59c0.63-0.63,0.18-1.71-0.71-1.71H9.41 - C8.52,10,8.08,11.08,8.71,11.71z"/> -</g> -</svg> diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/SetAsHomepageButton/SetAsHomepageButton.tsx b/packages/app-page-builder/src/pageEditor/config/editorBar/SetAsHomepageButton/SetAsHomepageButton.tsx deleted file mode 100644 index 4cdd8565cf8..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/SetAsHomepageButton/SetAsHomepageButton.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useCallback } from "react"; -import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar"; -import { ReactComponent as HomeIcon } from "~/admin/assets/round-home-24px.svg"; -import { usePageBuilderSettings } from "~/admin/hooks/usePageBuilderSettings"; -import { useAdminPageBuilder } from "~/admin/hooks/useAdminPageBuilder"; -import { usePage } from "~/pageEditor/hooks/usePage"; -import { createDecorator } from "@webiny/react-composition"; -import { PageOptionsMenu } from "~/pageEditor"; -import { useConfirmationDialog } from "@webiny/app-admin"; -import { useNavigatePage } from "~/admin/hooks/useNavigatePage"; -import { MenuItem } from "@webiny/ui/Menu"; -import { ListItemGraphic } from "@webiny/ui/List"; -import { Icon } from "@webiny/ui/Icon"; - -export const SetAsHomepageButtonPlugin = createDecorator(PageOptionsMenu, Original => { - return function SetAsHomepageButton({ items, ...props }) { - const [page] = usePage(); - const { navigateToLatestFolder } = useNavigatePage(); - const { showSnackbar } = useSnackbar(); - const pageBuilder = useAdminPageBuilder(); - const { showConfirmation } = useConfirmationDialog({ - message: ( - <span> - You're about to set this page as your new homepage, are you sure you want to - continue? - <br /> - Note that the page will automatically be published. - </span> - ) - }); - - const { settings, updateSettingsMutation, isSpecialPage } = usePageBuilderSettings(); - - const setPageAsHomepage = useCallback(async () => { - const publishPageResult = await pageBuilder.publishPage(page as { id: string }, { - client: pageBuilder.client - }); - /** - * In case of exit in "publishPage" lifecycle, "publishPage" hook will return undefined, - * indicating an immediate exit. - */ - if (!publishPageResult) { - return; - } - - if (publishPageResult.error) { - return showSnackbar(publishPageResult.error.message); - } - - const [updateSettings] = updateSettingsMutation; - const response = await updateSettings({ - variables: { - data: { - pages: { - ...settings.pages, - home: page.id - } - } - } - }); - - const { error } = response.data.pageBuilder.updateSettings; - if (error) { - return showSnackbar(error.message); - } - - navigateToLatestFolder(); - - // Let's wait a bit, because we are also redirecting the user. - setTimeout(() => showSnackbar("New homepage set successfully!"), 500); - }, [page.id]); - - const setAsHomepageItem = ( - <MenuItem - onClick={() => showConfirmation(setPageAsHomepage)} - disabled={isSpecialPage(page.pid!, "home")} - > - <ListItemGraphic> - <Icon icon={<HomeIcon />} /> - </ListItemGraphic> - Set as homepage - </MenuItem> - ); - - return <Original {...props} items={[setAsHomepageItem, ...items]} />; - }; -}); diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/Title/index.ts b/packages/app-page-builder/src/pageEditor/config/editorBar/Title/index.ts deleted file mode 100644 index 2a7f3c58777..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/Title/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./Title"; diff --git a/packages/app-page-builder/src/pageEditor/config/editorBar/index.ts b/packages/app-page-builder/src/pageEditor/config/editorBar/index.ts deleted file mode 100644 index b01e258dbc0..00000000000 --- a/packages/app-page-builder/src/pageEditor/config/editorBar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { EditorBarPlugins } from "./EditorBarPlugins"; diff --git a/packages/app-page-builder/src/pageEditor/config/eventActions/EventActionHandlerPlugin.tsx b/packages/app-page-builder/src/pageEditor/config/eventActions/EventActionHandlerPlugin.tsx index 6bf083e918e..25a7d355b24 100644 --- a/packages/app-page-builder/src/pageEditor/config/eventActions/EventActionHandlerPlugin.tsx +++ b/packages/app-page-builder/src/pageEditor/config/eventActions/EventActionHandlerPlugin.tsx @@ -7,8 +7,8 @@ import { } from "~/editor/contexts/EventActionHandlerProvider"; import { usePage } from "~/pageEditor/hooks/usePage"; import { useRevisions } from "~/pageEditor/hooks/useRevisions"; -import { useTemplateMode } from "~/pageEditor/hooks/useTemplateMode"; -import { PageAtomType, RevisionsAtomType, TemplateModeAtomType } from "~/pageEditor/state"; +import { TemplateModeAtomType, useTemplateMode } from "~/pageEditor/hooks/useTemplateMode"; +import { PageAtomType, RevisionsAtomType } from "~/pageEditor/state"; import { PageEditorEventActionCallableState } from "~/pageEditor/types"; import { PbElement, PbEditorElement } from "~/types"; diff --git a/packages/app-page-builder/src/pageEditor/createStateInitializer.ts b/packages/app-page-builder/src/pageEditor/createStateInitializer.ts index 4e98bc500a6..953075ef7af 100644 --- a/packages/app-page-builder/src/pageEditor/createStateInitializer.ts +++ b/packages/app-page-builder/src/pageEditor/createStateInitializer.ts @@ -4,10 +4,10 @@ import { PageAtomType, PageWithContent, revisionsAtom, - RevisionsAtomType, - templateModeAtom + RevisionsAtomType } from "~/pageEditor/state"; import { EditorStateInitializerFactory } from "~/editor/Editor"; +import { templateModeAtom } from "./hooks/useTemplateMode"; export const createStateInitializer = ( page: PageWithContent, diff --git a/packages/app-page-builder/src/pageEditor/editorConfig/PageEditorConfig.tsx b/packages/app-page-builder/src/pageEditor/editorConfig/PageEditorConfig.tsx new file mode 100644 index 00000000000..e82fc90798f --- /dev/null +++ b/packages/app-page-builder/src/pageEditor/editorConfig/PageEditorConfig.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { CompositionScope } from "@webiny/app-admin"; +import { EditorConfig } from "~/editor/config"; + +interface PageEditorConfigProps { + children: React.ReactNode; +} + +const BasePageEditorConfig = ({ children }: PageEditorConfigProps) => { + return ( + <CompositionScope name={"pb.pageEditor"}> + <EditorConfig>{children}</EditorConfig> + </CompositionScope> + ); +}; + +export const PageEditorConfig = Object.assign(BasePageEditorConfig, EditorConfig); diff --git a/packages/app-page-builder/src/pageEditor/hooks/usePageSettings.ts b/packages/app-page-builder/src/pageEditor/hooks/usePageSettings.ts index 394bf24a184..f0b66f37d0a 100644 --- a/packages/app-page-builder/src/pageEditor/hooks/usePageSettings.ts +++ b/packages/app-page-builder/src/pageEditor/hooks/usePageSettings.ts @@ -1,12 +1,11 @@ import { useCallback, useEffect, useState } from "react"; -import { useRecoilState } from "recoil"; import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar"; import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; import { useKeyHandler } from "~/editor/hooks/useKeyHandler"; import { UpdateDocumentActionEvent } from "~/editor/recoil/actions"; -import { pageSettingsStateAtom } from "~/pageEditor/config/editorBar/PageSettings/state"; import { usePage } from "~/pageEditor/hooks/usePage"; import { UpdatedPage } from "~/pageEditor/config/eventActions/saveRevision/types"; +import { usePageSettings as usePageSettingsState } from "~/pageEditor/config/TopBar/PageSettings/usePageSettings"; export type UsePageSettings = ReturnType<typeof usePageSettings>; @@ -14,15 +13,11 @@ export function usePageSettings() { const [activeSection, setActiveSection] = useState<string | null>(null); const eventActionHandler = useEventActionHandler(); const [pageData, setPageData] = usePage(); - const [, setSettingsState] = useRecoilState(pageSettingsStateAtom); + const { closeSettings } = usePageSettingsState(); const { showSnackbar } = useSnackbar(); const { removeKeyHandler, addKeyHandler } = useKeyHandler(); - const closeSettings = useCallback(() => { - setSettingsState(false); - }, []); - const savePage = useCallback(pageValue => { eventActionHandler.trigger( new UpdateDocumentActionEvent({ diff --git a/packages/app-page-builder/src/pageEditor/hooks/useTemplateMode.ts b/packages/app-page-builder/src/pageEditor/hooks/useTemplateMode.ts index 012bf810ac2..43006d16168 100644 --- a/packages/app-page-builder/src/pageEditor/hooks/useTemplateMode.ts +++ b/packages/app-page-builder/src/pageEditor/hooks/useTemplateMode.ts @@ -1,5 +1,10 @@ -import { useRecoilState } from "recoil"; -import { templateModeAtom } from "../state"; +import { atom, useRecoilState } from "recoil"; + +export type TemplateModeAtomType = boolean; +export const templateModeAtom = atom<TemplateModeAtomType>({ + key: "isTemplateMode", + default: false +}); export function useTemplateMode() { return useRecoilState(templateModeAtom); diff --git a/packages/app-page-builder/src/pageEditor/index.ts b/packages/app-page-builder/src/pageEditor/index.ts index 49237283456..17c7b1a1522 100644 --- a/packages/app-page-builder/src/pageEditor/index.ts +++ b/packages/app-page-builder/src/pageEditor/index.ts @@ -1,3 +1,2 @@ -export { PublishPageButton } from "./config/editorBar/PublishPageButton"; -export { PageOptionsMenu } from "./config/editorBar/PageOptionsMenu"; +export { PublishPageButton } from "./config/TopBar/PublishPageButton/PublishPageButton"; export { usePage } from "./hooks/usePage"; diff --git a/packages/app-page-builder/src/pageEditor/plugins/elementSettings/UnlinkBlockAction.ts b/packages/app-page-builder/src/pageEditor/plugins/elementSettings/UnlinkBlockAction.ts index 8b931519c22..e4bc3cf62c3 100644 --- a/packages/app-page-builder/src/pageEditor/plugins/elementSettings/UnlinkBlockAction.ts +++ b/packages/app-page-builder/src/pageEditor/plugins/elementSettings/UnlinkBlockAction.ts @@ -9,11 +9,13 @@ import { PbElement } from "~/types"; interface UnlinkBlockActionPropsType { children: React.ReactElement; } + const UnlinkBlockAction = ({ children }: UnlinkBlockActionPropsType) => { const [element] = useActiveElement(); const { getElementTree } = useEventActionHandler(); const updateElement = useUpdateElement(); + // TODO: extract block unlinking logic into a dedicated hook const onClick = useCallback(async (): Promise<void> => { if (element) { // we need to drop blockId and variables properties when unlinking, so they are separated from all other element data diff --git a/packages/app-page-builder/src/pageEditor/state/index.ts b/packages/app-page-builder/src/pageEditor/state/index.ts index 6618576751d..d917e59e2bf 100644 --- a/packages/app-page-builder/src/pageEditor/state/index.ts +++ b/packages/app-page-builder/src/pageEditor/state/index.ts @@ -1,3 +1,2 @@ export * from "./page"; export * from "./revisions"; -export * from "./templateMode"; diff --git a/packages/app-page-builder/src/pageEditor/state/templateMode/index.ts b/packages/app-page-builder/src/pageEditor/state/templateMode/index.ts deleted file mode 100644 index 594127f6037..00000000000 --- a/packages/app-page-builder/src/pageEditor/state/templateMode/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./templateModeAtom"; diff --git a/packages/app-page-builder/src/pageEditor/state/templateMode/templateModeAtom.ts b/packages/app-page-builder/src/pageEditor/state/templateMode/templateModeAtom.ts deleted file mode 100644 index 2b29854550e..00000000000 --- a/packages/app-page-builder/src/pageEditor/state/templateMode/templateModeAtom.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { atom } from "recoil"; - -export type TemplateModeAtomType = boolean; -export const templateModeAtom = atom<TemplateModeAtomType>({ - key: "isTemplateMode", - default: false -}); diff --git a/packages/app-page-builder/src/pageEditor/types.ts b/packages/app-page-builder/src/pageEditor/types.ts index 5f3a4c72a67..8c3350b16ea 100644 --- a/packages/app-page-builder/src/pageEditor/types.ts +++ b/packages/app-page-builder/src/pageEditor/types.ts @@ -1,5 +1,6 @@ import { EventActionCallable } from "~/types"; -import { PageAtomType, TemplateModeAtomType } from "~/pageEditor/state"; +import { PageAtomType } from "~/pageEditor/state"; +import { TemplateModeAtomType } from "~/pageEditor/hooks/useTemplateMode"; export interface PageEditorEventActionCallableState { page: PageAtomType; diff --git a/packages/app-page-builder/src/templateEditor/Editor.tsx b/packages/app-page-builder/src/templateEditor/Editor.tsx index 85e8095b67e..d7dacce32cb 100644 --- a/packages/app-page-builder/src/templateEditor/Editor.tsx +++ b/packages/app-page-builder/src/templateEditor/Editor.tsx @@ -24,7 +24,8 @@ import { PbErrorResponse, PbBlockCategory, PbEditorElement, PbPageTemplate } fro import createBlockCategoryPlugin from "~/admin/utils/createBlockCategoryPlugin"; import { PageTemplateWithContent } from "~/templateEditor/state"; import { createStateInitializer } from "./createStateInitializer"; -import { TemplateEditorConfig } from "./config/TemplateEditorConfig"; +import { DefaultEditorConfig } from "~/editor/defaultConfig/DefaultEditorConfig"; +import { DefaultTemplateEditorConfig } from "./config/DefaultTemplateEditorConfig"; import elementVariableRendererPlugins from "~/blockEditor/plugins/elementVariables"; import { usePageBlocks } from "~/admin/contexts/AdminPageBuilder/PageBlocks/usePageBlocks"; @@ -126,7 +127,8 @@ export const TemplateEditor = () => { return ( <React.Suspense fallback={<EditorLoadingScreen />}> - <TemplateEditorConfig /> + <DefaultEditorConfig /> + <DefaultTemplateEditorConfig /> <LoadData> <PbEditor stateInitializerFactory={createStateInitializer( diff --git a/packages/app-page-builder/src/templateEditor/config/BlockElementSidebarPlugin.tsx b/packages/app-page-builder/src/templateEditor/config/BlockElementSidebarPlugin.tsx deleted file mode 100644 index ebefac5093c..00000000000 --- a/packages/app-page-builder/src/templateEditor/config/BlockElementSidebarPlugin.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React from "react"; -import styled from "@emotion/styled"; -import { EditorSidebarTab } from "~/editor"; -import { createDecorator } from "@webiny/app-admin"; -import { useActiveElement } from "~/editor/hooks/useActiveElement"; -import { ButtonPrimary } from "@webiny/ui/Button"; -import UnlinkBlockAction from "~/pageEditor/plugins/elementSettings/UnlinkBlockAction"; -import { ReactComponent as InfoIcon } from "@webiny/app-admin/assets/icons/info.svg"; -import { RootElement } from "~/editor/components/Editor/Sidebar/ElementSettingsTabContent"; - -type UnlinkBlockWrapperProps = { - permission: boolean; -}; - -const UnlinkBlockWrapper = styled("div")<UnlinkBlockWrapperProps>` - padding: 16px; - display: grid; - row-gap: 16px; - justify-content: center; - align-items: center; - margin: auto 16px 16px 16px; - text-align: center; - background-color: var(--mdc-theme-background); - border: 3px dashed var(--webiny-theme-color-border); - border-radius: 5px; - opacity: ${props => !props.permission && "0.5"}; - - & .button-wrapper { - font-weight: bold; - } - - & .info-wrapper { - display: flex; - align-items: center; - font-size: 10px; - - & svg { - width: 18px; - margin-right: 5px; - } - } -`; - -type UnlinkTabProps = { - permission: boolean; -}; - -const UnlinkTab = ({ permission }: UnlinkTabProps) => { - return ( - <RootElement> - <UnlinkBlockWrapper permission={permission}> - This is a block element - to change it you need to unlink it first. By unlinking it, - any changes made to the block will no longer automatically reflect to this template. - <div className="button-wrapper"> - {permission ? ( - <UnlinkBlockAction> - <ButtonPrimary>Unlink block</ButtonPrimary> - </UnlinkBlockAction> - ) : ( - "No permissions" - )} - </div> - <div className="info-wrapper"> - <InfoIcon /> Click here to learn more about how block work - </div> - </UnlinkBlockWrapper> - </RootElement> - ); -}; - -export const BlockElementSidebarPlugin = createDecorator(EditorSidebarTab, Tab => { - return function ElementTab({ children, ...props }) { - const [element] = useActiveElement(); - - const unlinkPermission = true; - // TODO: check if the above check even works. - // const unlinkPermission = useMemo((): boolean => { - // const permission = getPermission<PageBuilderSecurityPermission>("pb.block.unlink"); - // if (permission?.name === "*" || permission?.name === "pb.*") { - // return true; - // } - // return Boolean(permission); - // }, [identity]); - - const isReferenceBlock = - element !== null && element.type === "block" && !!element.data?.blockId; - const isStyleTab = props?.label === "Style"; - - return ( - <Tab {...props}> - {isReferenceBlock && isStyleTab ? ( - <UnlinkTab permission={unlinkPermission} /> - ) : ( - children - )} - </Tab> - ); - }; -}); diff --git a/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/AddBlock.tsx b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/AddBlock.tsx new file mode 100644 index 00000000000..97fd0b5c672 --- /dev/null +++ b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/AddBlock.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import styled from "@emotion/styled"; +import { ButtonFloating } from "@webiny/ui/Button"; +import { ReactComponent as AddIcon } from "@material-design-icons/svg/round/add.svg"; +import { useBlocksBrowser } from "./useBlocksBrowser"; + +const SIDEBAR_WIDTH = 300; +const BottomRight = styled("div")({ + position: "fixed", + zIndex: 25, + bottom: 20, + right: 20 + SIDEBAR_WIDTH +}); + +export const AddBlock = () => { + const { openBrowser } = useBlocksBrowser(); + + return ( + <BottomRight> + <ButtonFloating onClick={openBrowser} icon={<AddIcon />} /> + </BottomRight> + ); +}; diff --git a/packages/app-page-builder/src/pageEditor/config/blockEditing/AddContent.tsx b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/AddContent.tsx similarity index 83% rename from packages/app-page-builder/src/pageEditor/config/blockEditing/AddContent.tsx rename to packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/AddContent.tsx index 5cf6d9ac171..7a94b58befd 100644 --- a/packages/app-page-builder/src/pageEditor/config/blockEditing/AddContent.tsx +++ b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/AddContent.tsx @@ -1,13 +1,13 @@ import React from "react"; -import { useRecoilValue, useRecoilState } from "recoil"; +import { useRecoilValue } from "recoil"; import { keyframes } from "emotion"; import styled from "@emotion/styled"; import { Elevation } from "@webiny/ui/Elevation"; import { ButtonFloating } from "@webiny/ui/Button"; -import { ReactComponent as AddIcon } from "~/editor/assets/icons/add.svg"; +import { ReactComponent as AddIcon } from "@material-design-icons/svg/round/add.svg"; import { useDisplayMode } from "~/editor/hooks/useDisplayMode"; import { elementsInContentTotalSelector } from "~/pageEditor/state"; -import { blocksBrowserStateAtom } from "~/pageEditor/config/blockEditing/state"; +import { useBlocksBrowser } from "./useBlocksBrowser"; const pulse = keyframes` 0% { @@ -56,14 +56,10 @@ const AddBlockContent = styled.div<{ displayMode: string }>(({ displayMode }) => alignItems: "center" })); -const AddContent = () => { +export const AddContent = () => { + const { openBrowser } = useBlocksBrowser(); const { displayMode } = useDisplayMode(); const totalElements = useRecoilValue(elementsInContentTotalSelector); - const [, setBlocksBrowserState] = useRecoilState(blocksBrowserStateAtom); - - const onClickHandler = () => { - setBlocksBrowserState(true); - }; if (totalElements) { return null; @@ -79,7 +75,7 @@ const AddContent = () => { style={{ animation: pulse + " 3s ease infinite", margin: "0 10px" }} small icon={<AddIcon />} - onClick={onClickHandler} + onClick={openBrowser} /> to start adding content </AddBlockContent> @@ -87,5 +83,3 @@ const AddContent = () => { </AddBlockContainer> ); }; - -export default AddContent; diff --git a/packages/app-page-builder/src/pageEditor/config/blockEditing/BlockPreview.tsx b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/BlockPreview.tsx similarity index 93% rename from packages/app-page-builder/src/pageEditor/config/blockEditing/BlockPreview.tsx rename to packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/BlockPreview.tsx index bfc2da44dc1..5f40c58b662 100644 --- a/packages/app-page-builder/src/pageEditor/config/blockEditing/BlockPreview.tsx +++ b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/BlockPreview.tsx @@ -6,8 +6,8 @@ import { Tooltip } from "@webiny/ui/Tooltip"; import { Typography } from "@webiny/ui/Typography"; import { CircularProgress } from "@webiny/ui/Progress"; import { ConfirmationDialog } from "@webiny/ui/ConfirmationDialog"; -import { ReactComponent as EditIcon } from "./icons/round-edit-24px.svg"; -import { ReactComponent as DeleteIcon } from "./icons/delete-24dpx.svg"; +import { ReactComponent as EditIcon } from "@material-design-icons/svg/round/edit.svg"; +import { ReactComponent as DeleteIcon } from "@material-design-icons/svg/round/delete.svg"; import * as Styled from "./StyledComponents"; import kebabCase from "lodash/kebabCase"; import { PbEditorBlockPlugin } from "~/types"; @@ -20,7 +20,7 @@ interface BlockPreviewProps { onDelete: (ev: React.MouseEvent) => void; } -const BlockPreview = (props: BlockPreviewProps) => { +export const BlockPreview = (props: BlockPreviewProps) => { const { plugin, addBlockToContent, onEdit, onDelete } = props; const onClickToAddHandler = useCallback(() => { addBlockToContent(plugin); @@ -88,7 +88,3 @@ const BlockPreview = (props: BlockPreviewProps) => { </Elevation> ); }; - -BlockPreview.displayName = "BlockPreview"; - -export default BlockPreview; diff --git a/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/BlocksBrowser.tsx b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/BlocksBrowser.tsx new file mode 100644 index 00000000000..06915be5666 --- /dev/null +++ b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/BlocksBrowser.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import { SearchBlocks } from "./SearchBlocks"; +import { useBlocksBrowser } from "./useBlocksBrowser"; + +export const BlocksBrowser = () => { + const { isOpen, closeBrowser } = useBlocksBrowser(); + + return isOpen ? <SearchBlocks onClose={closeBrowser} /> : null; +}; diff --git a/packages/app-page-builder/src/pageEditor/config/blockEditing/BlocksList.tsx b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/BlocksList.tsx similarity index 96% rename from packages/app-page-builder/src/pageEditor/config/blockEditing/BlocksList.tsx rename to packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/BlocksList.tsx index 5ebe0e096bc..d4ef973b603 100644 --- a/packages/app-page-builder/src/pageEditor/config/blockEditing/BlocksList.tsx +++ b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/BlocksList.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef, useState } from "react"; import { useInViewport } from "react-in-viewport"; import styled from "@emotion/styled"; -import BlockPreview from "./BlockPreview"; +import { BlockPreview } from "./BlockPreview"; import { PbEditorBlockPlugin } from "~/types"; import { ResponsiveElementsProvider } from "~/admin/components/ResponsiveElementsProvider"; @@ -57,7 +57,7 @@ interface BlocksListProps extends Omit<RenderRowProps, "index" | "key" | "style" category: string; } -const BlocksList = (props: BlocksListProps) => { +export const BlocksList = (props: BlocksListProps) => { const rightPanelElement = useRef<HTMLElement | null>(null); const prevProps = useRef<BlocksListProps | null>(null); @@ -109,5 +109,3 @@ const BlocksList = (props: BlocksListProps) => { </BlocksResponsiveContainer> ); }; - -export default BlocksList; diff --git a/packages/app-page-builder/src/pageEditor/config/blockEditing/EditBlockDialog.tsx b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/EditBlockDialog.tsx similarity index 100% rename from packages/app-page-builder/src/pageEditor/config/blockEditing/EditBlockDialog.tsx rename to packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/EditBlockDialog.tsx index f7d078405a0..2e5b1652e8a 100644 --- a/packages/app-page-builder/src/pageEditor/config/blockEditing/EditBlockDialog.tsx +++ b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/EditBlockDialog.tsx @@ -15,8 +15,8 @@ import { CircularProgress } from "@webiny/ui/Progress"; import { Grid, Cell } from "@webiny/ui/Grid"; import { Form, FormOnSubmit } from "@webiny/form"; import styled from "@emotion/styled"; -import { PbEditorBlockCategoryPlugin, PbEditorBlockPlugin } from "~/types"; import { ButtonPrimary } from "@webiny/ui/Button"; +import { PbEditorBlockCategoryPlugin, PbEditorBlockPlugin } from "~/types"; const StyledDialog = styled(Dialog)` // We need to have this z-index because without it Edit Block Dialog will be rendered below All Blocks Component. diff --git a/packages/app-page-builder/src/pageEditor/config/blockEditing/SearchBlocks.tsx b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/SearchBlocks.tsx similarity index 94% rename from packages/app-page-builder/src/pageEditor/config/blockEditing/SearchBlocks.tsx rename to packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/SearchBlocks.tsx index 09f5a3e643e..556b0fd978a 100644 --- a/packages/app-page-builder/src/pageEditor/config/blockEditing/SearchBlocks.tsx +++ b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/SearchBlocks.tsx @@ -14,10 +14,10 @@ import { SimpleFormContent, SimpleFormHeader } from "@webiny/app-admin/components/SimpleForm"; -import { useRecoilState } from "recoil"; -import { ReactComponent as AllIcon } from "./icons/round-clear_all-24px.svg"; -import BlocksList from "./BlocksList"; +import { ReactComponent as AllIcon } from "@material-design-icons/svg/round/clear_all.svg"; +import { DelayedOnChange } from "@webiny/ui/DelayedOnChange"; +import { BlocksList } from "./BlocksList"; import EditBlockDialog from "./EditBlockDialog"; import { IconWrapper, @@ -34,8 +34,6 @@ import { useKeyHandler } from "~/editor/hooks/useKeyHandler"; import { UpdateElementActionEvent } from "~/editor/recoil/actions"; import { createBlockElements } from "~/editor/helpers"; import { createBlockReference } from "~/pageEditor/helpers"; -import { DelayedOnChange } from "@webiny/ui/DelayedOnChange"; -import { blocksBrowserStateAtom } from "~/pageEditor/config/blockEditing/state"; import { usePageBlocks } from "~/admin/contexts/AdminPageBuilder/PageBlocks/usePageBlocks"; import { useRootElement } from "~/editor/hooks/useRootElement"; @@ -68,8 +66,11 @@ const sortBlocks = (blocks: PbEditorBlockPlugin[]): PbEditorBlockPlugin[] => { }); }; -const SearchBar = () => { - const [, setBlocksBrowserState] = useRecoilState(blocksBrowserStateAtom); +export interface SearchBarProps { + onClose: () => void; +} + +export const SearchBlocks = ({ onClose }: SearchBarProps) => { const eventActionHandler = useEventActionHandler(); const { showSnackbar } = useSnackbar(); const content = useRootElement(); @@ -124,14 +125,10 @@ const SearchBar = () => { const { addKeyHandler, removeKeyHandler } = useKeyHandler(); - const deactivatePlugin = () => { - setBlocksBrowserState(false); - }; - useEffect(() => { addKeyHandler("escape", e => { e.preventDefault(); - deactivatePlugin(); + onClose(); }); return () => removeKeyHandler("escape"); @@ -155,7 +152,7 @@ const SearchBar = () => { }) ); - deactivatePlugin(); + onClose(); }, [content] ); @@ -254,17 +251,13 @@ const SearchBar = () => { ); }, [search]); - const onExited = useCallback(() => { - deactivatePlugin(); - }, []); - const categoryPlugin = allCategories.find(pl => pl.categoryName === activeCategory) || { title: null, icon: <></> }; return ( - <OverlayLayout barMiddle={renderSearchInput()} onExited={onExited}> + <OverlayLayout barMiddle={renderSearchInput()} onExited={onClose}> <SplitView> <LeftPanel span={3}> <ScrollList className={listStyle}> @@ -325,5 +318,3 @@ const SearchBar = () => { </OverlayLayout> ); }; - -export default SearchBar; diff --git a/packages/app-page-builder/src/pageEditor/config/blockEditing/SearchBlocksStyled.tsx b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/SearchBlocksStyled.tsx similarity index 100% rename from packages/app-page-builder/src/pageEditor/config/blockEditing/SearchBlocksStyled.tsx rename to packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/SearchBlocksStyled.tsx diff --git a/packages/app-page-builder/src/pageEditor/config/blockEditing/StyledComponents.ts b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/StyledComponents.ts similarity index 100% rename from packages/app-page-builder/src/pageEditor/config/blockEditing/StyledComponents.ts rename to packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/StyledComponents.ts diff --git a/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/useBlocksBrowser.ts b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/useBlocksBrowser.ts new file mode 100644 index 00000000000..f2f37a34e03 --- /dev/null +++ b/packages/app-page-builder/src/templateEditor/config/Content/BlocksBrowser/useBlocksBrowser.ts @@ -0,0 +1,23 @@ +import { useCallback } from "react"; +import { atom, useRecoilState } from "recoil"; + +type BlocksBrowserState = boolean; + +const stateAtom = atom<BlocksBrowserState>({ + key: "blocksBrowserStateAtom", + default: false +}); + +export function useBlocksBrowser() { + const [isOpen, setOpen] = useRecoilState(stateAtom); + + const openBrowser = useCallback(() => { + setOpen(true); + }, []); + + const closeBrowser = useCallback(() => { + setOpen(false); + }, []); + + return { isOpen, openBrowser, closeBrowser }; +} diff --git a/packages/app-page-builder/src/templateEditor/config/DefaultTemplateEditorConfig.tsx b/packages/app-page-builder/src/templateEditor/config/DefaultTemplateEditorConfig.tsx new file mode 100644 index 00000000000..fd3a49c8bad --- /dev/null +++ b/packages/app-page-builder/src/templateEditor/config/DefaultTemplateEditorConfig.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { EventActionHandlerPlugin, EventActionPlugins } from "./eventActions"; +import { TemplateEditorConfig } from "../editorConfig/TemplateEditorConfig"; +import { BackButton } from "./TopBar/BackButton/BackButton"; +import { SaveTemplateButton } from "./TopBar/SaveTemplateButton/SaveTemplateButton"; +import { TemplateSettingsButton } from "./TopBar/TemplateSettingsButton/TemplateSettingsButton"; +import { Title } from "./TopBar/Title/Title"; +import { BlocksBrowser } from "./Content/BlocksBrowser/BlocksBrowser"; +import { AddBlock } from "./Content/BlocksBrowser/AddBlock"; +import { AddContent } from "./Content/BlocksBrowser/AddContent"; +import { UnlinkBlock } from "./Sidebar/UnlinkBlock"; +import { ElementSettingsGroup } from "./Sidebar/ElementSettingsGroup"; +import { RefreshBlockAction } from "./Sidebar/RefreshBlockAction"; +import { EditBlockAction } from "./Sidebar/EditBlockAction"; +import { HideSaveAction } from "./Sidebar/HideSaveAction"; + +const { TopBar, Toolbar, Content, Sidebar, Element } = TemplateEditorConfig; + +export const DefaultTemplateEditorConfig = React.memo(() => { + return ( + <> + <EventActionHandlerPlugin /> + <EventActionPlugins /> + <TemplateEditorConfig> + <TopBar.Element name={"buttonBack"} group={"left"} element={<BackButton />} /> + <TopBar.Element name={"title"} group={"left"} element={<Title />} /> + <TopBar.Action + name={"buttonTemplateSettings"} + element={<TemplateSettingsButton />} + /> + <TopBar.Action name={"buttonSaveTemplate"} element={<SaveTemplateButton />} /> + <Toolbar.Element name={"savingIndicator"} remove /> + <Content.Element name={"addBlock"} element={<AddBlock />} /> + <Content.Element name={"addContent"} element={<AddContent />} /> + <Element group={"overlays"} name={"blocksBrowser"} element={<BlocksBrowser />} /> + <Sidebar.ElementAction name={"editBlock"} element={<EditBlockAction />} /> + <Sidebar.ElementAction name={"refreshBlock"} element={<RefreshBlockAction />} /> + <Sidebar.Element + name={"blockActions"} + group={"element"} + element={<ElementSettingsGroup />} + /> + <HideSaveAction /> + <UnlinkBlock /> + </TemplateEditorConfig> + </> + ); +}); + +DefaultTemplateEditorConfig.displayName = "DefaultTemplateEditorConfig"; diff --git a/packages/app-page-builder/src/templateEditor/config/ElementSettingsTabContentPlugin.tsx b/packages/app-page-builder/src/templateEditor/config/ElementSettingsTabContentPlugin.tsx deleted file mode 100644 index 64d7e068023..00000000000 --- a/packages/app-page-builder/src/templateEditor/config/ElementSettingsTabContentPlugin.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from "react"; -import { ReactComponent as EditIcon } from "@material-design-icons/svg/round/edit.svg"; -import { ReactComponent as RefreshIcon } from "@material-design-icons/svg/round/refresh.svg"; -import { plugins } from "@webiny/plugins"; -import { SidebarActions } from "~/editor"; -import { createDecorator } from "@webiny/app-admin"; -import Action from "~/editor/plugins/elementSettings/components/Action"; -import { useActiveElement } from "~/editor/hooks/useActiveElement"; -import ElementNotLinked from "~/blockEditor/components/elementSettingsTab/ElementNotLinked"; -import VariableSettings from "~/blockEditor/components/elementSettingsTab/VariableSettings"; -import VariablesList from "~/blockEditor/components/elementSettingsTab/VariablesList"; -import useElementSettings from "~/editor/plugins/elementSettings/hooks/useElementSettings"; -import { useRefreshBlock } from "~/editor/hooks/useRefreshBlock"; -import { PbBlockEditorCreateVariablePlugin, PbEditorElement } from "~/types"; - -export const ElementSettingsTabContentPlugin = createDecorator( - SidebarActions, - SidebarActionsWrapper => { - const variablePlugins = plugins.byType<PbBlockEditorCreateVariablePlugin>( - "pb-block-editor-create-variable" - ); - - return function SettingsTabContent({ children, ...props }) { - const [element] = useActiveElement(); - const elementSettings = useElementSettings(); - const { refreshBlock, loading } = useRefreshBlock(element as PbEditorElement); - const canHaveVariable = - element && - variablePlugins.some(variablePlugin => variablePlugin.elementType === element.type); - const hasVariable = element && element.data?.variableId; - const isBlock = element && element.type === "block"; - const isReferenceBlock = element && element.data?.blockId; - - return ( - <> - <SidebarActionsWrapper {...props}> - {isReferenceBlock ? ( - <> - {elementSettings.map(({ plugin, options }, index) => { - return ( - <div key={plugin.name + "-" + index}> - {typeof plugin.renderAction === "function" && - plugin.name !== - "pb-editor-page-element-settings-save" && - plugin.renderAction({ options })} - </div> - ); - })} - <Action - tooltip={"Edit block"} - icon={<EditIcon />} - onClick={() => - window.open( - `/page-builder/block-editor/${element?.data?.blockId}`, - "_blank", - "noopener" - ) - } - /> - <Action - disabled={loading} - tooltip={loading ? "Refreshing..." : "Refresh block"} - onClick={refreshBlock} - icon={<RefreshIcon />} - /> - </> - ) : ( - children - )} - </SidebarActionsWrapper> - {isBlock && !isReferenceBlock && <VariablesList block={element} />} - {canHaveVariable && !hasVariable && <ElementNotLinked />} - {canHaveVariable && hasVariable && <VariableSettings element={element} />} - </> - ); - }; - } -); diff --git a/packages/app-page-builder/src/templateEditor/config/Sidebar/EditBlockAction.tsx b/packages/app-page-builder/src/templateEditor/config/Sidebar/EditBlockAction.tsx new file mode 100644 index 00000000000..46f8aca3b74 --- /dev/null +++ b/packages/app-page-builder/src/templateEditor/config/Sidebar/EditBlockAction.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { ReactComponent as EditIcon } from "@material-design-icons/svg/round/edit.svg"; +import { TemplateEditorConfig } from "~/templateEditor/editorConfig/TemplateEditorConfig"; +import { useBlockReference } from "./useBlockReference"; + +export const EditBlockAction = () => { + const blockReference = useBlockReference(); + + if (!blockReference) { + return null; + } + + return ( + <TemplateEditorConfig.Sidebar.ElementAction.IconButton + label={"Edit block"} + icon={<EditIcon />} + onClick={() => + window.open( + `/page-builder/block-editor/${blockReference.referencedBlockId}`, + "_blank", + "noopener" + ) + } + /> + ); +}; diff --git a/packages/app-page-builder/src/templateEditor/config/Sidebar/ElementSettingsGroup.tsx b/packages/app-page-builder/src/templateEditor/config/Sidebar/ElementSettingsGroup.tsx new file mode 100644 index 00000000000..b6eeab7d549 --- /dev/null +++ b/packages/app-page-builder/src/templateEditor/config/Sidebar/ElementSettingsGroup.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { plugins } from "@webiny/plugins"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; +import { ElementNotLinked } from "~/blockEditor/components/elementSettingsTab/ElementNotLinked"; +import VariableSettings from "~/blockEditor/components/elementSettingsTab/VariableSettings"; +import VariablesList from "~/blockEditor/components/elementSettingsTab/VariablesList"; +import { PbBlockEditorCreateVariablePlugin } from "~/types"; +import { useBlockReference } from "~/templateEditor/config/Sidebar/useBlockReference"; + +export const ElementSettingsGroup = () => { + const variablePlugins = plugins.byType<PbBlockEditorCreateVariablePlugin>( + "pb-block-editor-create-variable" + ); + + const [element] = useActiveElement(); + const blockReference = useBlockReference(); + + const canHaveVariable = element && variablePlugins.some(vp => vp.elementType === element.type); + const hasVariable = element && element.data?.variableId; + const isBlock = element && element.type === "block"; + + return ( + <> + {isBlock && !blockReference ? <VariablesList block={element} /> : null} + {canHaveVariable && !hasVariable && <ElementNotLinked />} + {canHaveVariable && hasVariable && <VariableSettings element={element} />} + </> + ); +}; diff --git a/packages/app-page-builder/src/templateEditor/config/Sidebar/HideSaveAction.tsx b/packages/app-page-builder/src/templateEditor/config/Sidebar/HideSaveAction.tsx new file mode 100644 index 00000000000..6b5174d06ac --- /dev/null +++ b/packages/app-page-builder/src/templateEditor/config/Sidebar/HideSaveAction.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { TemplateEditorConfig } from "~/templateEditor/editorConfig/TemplateEditorConfig"; +import { useBlockReference } from "~/templateEditor/config/Sidebar/useBlockReference"; + +const { Sidebar } = TemplateEditorConfig; + +interface HideIfBlockReferenceProps { + children: React.ReactNode; +} + +const HideIfBlockReference = ({ children }: HideIfBlockReferenceProps) => { + const blockReference = useBlockReference(); + + if (blockReference) { + return null; + } + + return <>{children}</>; +}; + +export const HideSaveAction = Sidebar.ElementAction.createDecorator(Original => { + return function SaveAction(props) { + if (props.name !== "save") { + return <Original {...props} />; + } + + return ( + <Original + {...props} + element={<HideIfBlockReference>{props.element}</HideIfBlockReference>} + /> + ); + }; +}); diff --git a/packages/app-page-builder/src/templateEditor/config/Sidebar/RefreshBlockAction.tsx b/packages/app-page-builder/src/templateEditor/config/Sidebar/RefreshBlockAction.tsx new file mode 100644 index 00000000000..f5ec26d8e3e --- /dev/null +++ b/packages/app-page-builder/src/templateEditor/config/Sidebar/RefreshBlockAction.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { ReactComponent as RefreshIcon } from "@material-design-icons/svg/round/refresh.svg"; +import { TemplateEditorConfig } from "~/templateEditor/editorConfig/TemplateEditorConfig"; +import { useBlockReference } from "./useBlockReference"; +import { useRefreshBlock } from "~/editor/hooks/useRefreshBlock"; + +export const RefreshBlockAction = () => { + const blockReference = useBlockReference(); + const { refreshBlock, loading } = useRefreshBlock(); + + if (!blockReference) { + return null; + } + + return ( + <TemplateEditorConfig.Sidebar.ElementAction.IconButton + label={loading ? "Refreshing..." : "Refresh block"} + icon={<RefreshIcon />} + onClick={() => refreshBlock(blockReference.block)} + /> + ); +}; diff --git a/packages/app-page-builder/src/templateEditor/config/Sidebar/UnlinkBlock.tsx b/packages/app-page-builder/src/templateEditor/config/Sidebar/UnlinkBlock.tsx new file mode 100644 index 00000000000..7ad3cfe648b --- /dev/null +++ b/packages/app-page-builder/src/templateEditor/config/Sidebar/UnlinkBlock.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import styled from "@emotion/styled"; +import { useActiveElement } from "~/editor/hooks/useActiveElement"; +import { ButtonPrimary } from "@webiny/ui/Button"; +import UnlinkBlockAction from "~/pageEditor/plugins/elementSettings/UnlinkBlockAction"; +import { ReactComponent as InfoIcon } from "@webiny/app-admin/assets/icons/info.svg"; +import { TemplateEditorConfig } from "~/templateEditor/editorConfig/TemplateEditorConfig"; + +type UnlinkBlockWrapperProps = { + permission: boolean; +}; + +const UnlinkBlockWrapper = styled("div")<UnlinkBlockWrapperProps>` + padding: 16px; + display: grid; + row-gap: 16px; + justify-content: center; + align-items: center; + margin: auto 16px 16px 16px; + text-align: center; + background-color: var(--mdc-theme-background); + border: 3px dashed var(--webiny-theme-color-border); + border-radius: 5px; + opacity: ${props => !props.permission && "0.5"}; + + & .button-wrapper { + font-weight: bold; + } + + & .info-wrapper { + display: flex; + align-items: center; + font-size: 10px; + + & svg { + width: 18px; + margin-right: 5px; + } + } +`; + +type UnlinkTabProps = { + permission: boolean; +}; + +const UnlinkTab = ({ permission }: UnlinkTabProps) => { + return ( + <UnlinkBlockWrapper permission={permission}> + This is a block element - to change it you need to unlink it first. By unlinking it, any + changes made to the block will no longer automatically reflect to this template. + <div className="button-wrapper"> + {permission ? ( + <UnlinkBlockAction> + <ButtonPrimary>Unlink block</ButtonPrimary> + </UnlinkBlockAction> + ) : ( + "No permissions" + )} + </div> + <div className="info-wrapper"> + <InfoIcon /> Click here to learn more about how block work + </div> + </UnlinkBlockWrapper> + ); +}; + +export const UnlinkBlock = TemplateEditorConfig.Sidebar.Elements.createDecorator(Original => { + return function SidebarElements(props) { + const [element] = useActiveElement(); + + if (!element || props.group !== "style") { + return <Original {...props} />; + } + + const isReferenceBlock = element.type === "block" && !!element.data?.blockId; + + if (isReferenceBlock) { + return <UnlinkTab permission={true} />; + } + + return <Original {...props} />; + }; +}); diff --git a/packages/app-page-builder/src/templateEditor/config/Sidebar/useBlockReference.ts b/packages/app-page-builder/src/templateEditor/config/Sidebar/useBlockReference.ts new file mode 100644 index 00000000000..bc92fd98f31 --- /dev/null +++ b/packages/app-page-builder/src/templateEditor/config/Sidebar/useBlockReference.ts @@ -0,0 +1,21 @@ +import { useActiveElement } from "~/editor/hooks/useActiveElement"; +import { PbEditorElement } from "~/types"; + +export interface BlockReference { + referencedBlockId: string; + block: PbEditorElement; +} + +export const useBlockReference = (): BlockReference | undefined => { + const [element] = useActiveElement(); + const referencedBlockId = element && element.data?.blockId; + + if (element?.type !== "block" || !referencedBlockId) { + return undefined; + } + + return { + referencedBlockId, + block: element + }; +}; diff --git a/packages/app-page-builder/src/templateEditor/config/TemplateEditorConfig.tsx b/packages/app-page-builder/src/templateEditor/config/TemplateEditorConfig.tsx deleted file mode 100644 index 6be7c335501..00000000000 --- a/packages/app-page-builder/src/templateEditor/config/TemplateEditorConfig.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import { EventActionPlugins, EventActionHandlerPlugin } from "./eventActions"; -import { EditorBarPlugins } from "./editorBar"; -import { BlockEditingPlugin } from "~/pageEditor/config/blockEditing"; -import { BlockElementSidebarPlugin } from "~/templateEditor/config/BlockElementSidebarPlugin"; -import { ToolbarActionsPlugin } from "~/blockEditor/config/ToolbarActionsPlugin"; -import { ElementSettingsTabContentPlugin } from "./ElementSettingsTabContentPlugin"; - -export const TemplateEditorConfig = React.memo(() => { - return ( - <> - <EventActionHandlerPlugin /> - <EditorBarPlugins /> - <EventActionPlugins /> - <BlockEditingPlugin /> - <BlockElementSidebarPlugin /> - <ElementSettingsTabContentPlugin /> - <ToolbarActionsPlugin /> - </> - ); -}); - -TemplateEditorConfig.displayName = "TemplateEditorConfig"; diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/BackButton/BackButton.tsx b/packages/app-page-builder/src/templateEditor/config/TopBar/BackButton/BackButton.tsx similarity index 58% rename from packages/app-page-builder/src/templateEditor/config/editorBar/BackButton/BackButton.tsx rename to packages/app-page-builder/src/templateEditor/config/TopBar/BackButton/BackButton.tsx index 75940795bd3..2e579186974 100644 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/BackButton/BackButton.tsx +++ b/packages/app-page-builder/src/templateEditor/config/TopBar/BackButton/BackButton.tsx @@ -1,30 +1,30 @@ import React, { useCallback } from "react"; import { css } from "emotion"; -import { createDecorator } from "@webiny/app-admin"; import { useNavigate } from "@webiny/react-router"; import { IconButton } from "@webiny/ui/Button"; -import { EditorBar } from "~/editor"; import { ReactComponent as BackIcon } from "@material-design-icons/svg/round/arrow_back.svg"; +import { TopBar } from "~/editor/config/TopBar/TopBar"; const backStyles = css({ marginLeft: -10 }); -export const BackButtonPlugin = createDecorator(EditorBar.BackButton, () => { - return function BackButton() { - const navigate = useNavigate(); +export function BackButton() { + const navigate = useNavigate(); - const onClick = useCallback(() => { - navigate(-1); - }, [navigate]); + const onClick = useCallback(() => { + navigate(-1); + }, [navigate]); - return ( + return ( + <> <IconButton data-testid="pb-editor-back-button" className={backStyles} onClick={onClick} icon={<BackIcon />} /> - ); - }; -}); + <TopBar.Divider /> + </> + ); +} diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/SaveTemplateButton/SaveTemplateButton.tsx b/packages/app-page-builder/src/templateEditor/config/TopBar/SaveTemplateButton/SaveTemplateButton.tsx similarity index 77% rename from packages/app-page-builder/src/templateEditor/config/editorBar/SaveTemplateButton/SaveTemplateButton.tsx rename to packages/app-page-builder/src/templateEditor/config/TopBar/SaveTemplateButton/SaveTemplateButton.tsx index 40b92322701..282e9d838d8 100644 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/SaveTemplateButton/SaveTemplateButton.tsx +++ b/packages/app-page-builder/src/templateEditor/config/TopBar/SaveTemplateButton/SaveTemplateButton.tsx @@ -1,11 +1,9 @@ import React, { useCallback, useState } from "react"; import styled from "@emotion/styled"; -import { createDecorator, makeDecoratable } from "@webiny/app-admin"; import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar"; import { useRouter } from "@webiny/react-router"; import { ButtonIcon, ButtonPrimary } from "@webiny/ui/Button"; import { CircularProgress } from "@webiny/ui/Progress"; -import { EditorBar } from "~/editor"; import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; import { useTemplate } from "~/templateEditor/hooks/useTemplate"; import { SaveTemplateActionEvent } from "~/templateEditor/config/eventActions/saveTemplate/event"; @@ -16,7 +14,7 @@ const SpinnerWrapper = styled.div` margin-left: -4px !important; `; -const DefaultSaveTemplateButton = () => { +export const SaveTemplateButton = () => { const [template] = useTemplate(); const eventActionHandler = useEventActionHandler(); const { history } = useRouter(); @@ -56,16 +54,3 @@ const DefaultSaveTemplateButton = () => { </ButtonPrimary> ); }; - -export const SaveTemplateButton = makeDecoratable("SaveTemplateButton", DefaultSaveTemplateButton); - -export const SaveTemplateButtonPlugin = createDecorator(EditorBar.RightSection, RightSection => { - return function AddSaveTemplateButton(props) { - return ( - <RightSection> - <SaveTemplateButton /> - {props.children} - </RightSection> - ); - }; -}); diff --git a/packages/app-page-builder/src/templateEditor/config/TopBar/TemplateSettingsButton/TemplateSettingsButton.tsx b/packages/app-page-builder/src/templateEditor/config/TopBar/TemplateSettingsButton/TemplateSettingsButton.tsx new file mode 100644 index 00000000000..230cddd336f --- /dev/null +++ b/packages/app-page-builder/src/templateEditor/config/TopBar/TemplateSettingsButton/TemplateSettingsButton.tsx @@ -0,0 +1,23 @@ +import React, { useState, useCallback } from "react"; +import { IconButton } from "@webiny/ui/Button"; +import { ReactComponent as SettingsIcon } from "@material-design-icons/svg/round/settings.svg"; +import TemplateSettingsModal from "./TemplateSettingsModal"; + +export const TemplateSettingsButton = () => { + const [open, setOpen] = useState(false); + + const onClick = useCallback(() => { + setOpen(true); + }, []); + + const onClose = useCallback(() => { + setOpen(false); + }, []); + + return ( + <> + <IconButton onClick={onClick} icon={<SettingsIcon />} /> + <TemplateSettingsModal open={open} onClose={onClose} /> + </> + ); +}; diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/TemplateSettingsModal.tsx b/packages/app-page-builder/src/templateEditor/config/TopBar/TemplateSettingsButton/TemplateSettingsModal.tsx similarity index 94% rename from packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/TemplateSettingsModal.tsx rename to packages/app-page-builder/src/templateEditor/config/TopBar/TemplateSettingsButton/TemplateSettingsModal.tsx index 232481abb81..b821af8d9fa 100644 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/TemplateSettingsModal.tsx +++ b/packages/app-page-builder/src/templateEditor/config/TopBar/TemplateSettingsButton/TemplateSettingsModal.tsx @@ -1,7 +1,6 @@ import React, { useCallback } from "react"; import slugify from "slugify"; import { css } from "emotion"; -import { useRecoilState } from "recoil"; import get from "lodash/get"; import pick from "lodash/pick"; import { useQuery } from "@apollo/react-hooks"; @@ -15,7 +14,6 @@ import { SimpleFormContent } from "@webiny/app-admin/components/SimpleForm"; import { validation } from "@webiny/validation"; import { Dialog, DialogCancel, DialogTitle, DialogActions, DialogContent } from "@webiny/ui/Dialog"; -import { templateSettingsStateAtom } from "./state"; import { useTemplate } from "~/templateEditor/hooks/useTemplate"; import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; import { UpdateDocumentActionEvent } from "~/editor/recoil/actions"; @@ -32,13 +30,14 @@ const narrowDialog = css` } `; -const TemplateSettingsModal = () => { +interface TemplateSettingsModalProps { + open: boolean; + onClose: () => void; +} + +const TemplateSettingsModal = (props: TemplateSettingsModalProps) => { const handler = useEventActionHandler(); const [template] = useTemplate(); - const [, setState] = useRecoilState(templateSettingsStateAtom); - const onClose = useCallback(() => { - setState(false); - }, []); const layouts = React.useMemo(() => { const layoutPlugins = plugins.byType<PbPageLayoutPlugin>("pb-page-layout"); @@ -80,7 +79,7 @@ const TemplateSettingsModal = () => { const onSubmit = useCallback(formData => { updateTemplate(formData); - onClose(); + props.onClose(); }, []); const settings = pick(template, [ @@ -95,7 +94,7 @@ const TemplateSettingsModal = () => { const loading = [pageCategoriesQuery].some(item => item.loading); return ( - <Dialog open={true} onClose={onClose} className={narrowDialog}> + <Dialog open={props.open} onClose={props.onClose} className={narrowDialog}> <Form data={settings} onSubmit={onSubmit}> {({ form, Bind }) => ( <> @@ -174,7 +173,7 @@ const TemplateSettingsModal = () => { </SimpleFormContent> </DialogContent> <DialogActions> - <DialogCancel onClick={onClose}>Cancel</DialogCancel> + <DialogCancel onClick={props.onClose}>Cancel</DialogCancel> <ButtonPrimary onClick={ev => { form.submit(ev); diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/Title/Styled.ts b/packages/app-page-builder/src/templateEditor/config/TopBar/Title/Styled.ts similarity index 100% rename from packages/app-page-builder/src/templateEditor/config/editorBar/Title/Styled.ts rename to packages/app-page-builder/src/templateEditor/config/TopBar/Title/Styled.ts diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/Title/Title.tsx b/packages/app-page-builder/src/templateEditor/config/TopBar/Title/Title.tsx similarity index 89% rename from packages/app-page-builder/src/templateEditor/config/editorBar/Title/Title.tsx rename to packages/app-page-builder/src/templateEditor/config/TopBar/Title/Title.tsx index 23bf43d9da3..58e215e87e2 100644 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/Title/Title.tsx +++ b/packages/app-page-builder/src/templateEditor/config/TopBar/Title/Title.tsx @@ -2,12 +2,10 @@ import React, { useState, useCallback, SyntheticEvent, useEffect } from "react"; import { useSnackbar } from "@webiny/app-admin/hooks/useSnackbar"; import { Input } from "@webiny/ui/Input"; import { Tooltip } from "@webiny/ui/Tooltip"; -import { createDecorator } from "@webiny/app-admin"; import { TemplateTitle, templateTitleWrapper, TitleInputWrapper, TitleWrapper } from "./Styled"; import { useEventActionHandler } from "~/editor/hooks/useEventActionHandler"; import { PageTemplate } from "~/templateEditor/state"; import { UpdateDocumentActionEvent } from "~/editor/recoil/actions"; -import { EditorBar } from "~/editor"; import { useTemplate } from "~/templateEditor/hooks/useTemplate"; declare global { @@ -16,7 +14,7 @@ declare global { } } -const Title = () => { +export const Title = () => { const handler = useEventActionHandler(); const [template] = useTemplate(); const { showSnackbar } = useSnackbar(); @@ -85,7 +83,7 @@ const Title = () => { const autoFocus = !window.Cypress; return editTitle ? ( - <TitleInputWrapper> + <TitleInputWrapper data-testid="pb-editor-page-title"> <Input autoFocus={autoFocus} fullwidth @@ -111,14 +109,3 @@ const Title = () => { </TitleWrapper> ); }; - -export const TitlePlugin = createDecorator(EditorBar.LeftSection, LeftSection => { - return function AddTitle(props) { - return ( - <LeftSection> - {props.children} - <Title /> - </LeftSection> - ); - }; -}); diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/BackButton/index.ts b/packages/app-page-builder/src/templateEditor/config/editorBar/BackButton/index.ts deleted file mode 100644 index 26e35db76f4..00000000000 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/BackButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./BackButton"; diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/EditorBarPlugins.tsx b/packages/app-page-builder/src/templateEditor/config/editorBar/EditorBarPlugins.tsx deleted file mode 100644 index 95d77193616..00000000000 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/EditorBarPlugins.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import { BackButtonPlugin } from "./BackButton"; -import { TemplateSettingsPlugin } from "./TemplateSettings"; -import { SaveTemplateButtonPlugin } from "./SaveTemplateButton"; -import { TitlePlugin } from "./Title"; - -export const EditorBarPlugins = () => { - return ( - <> - <BackButtonPlugin /> - <TitlePlugin /> - <TemplateSettingsPlugin /> - <SaveTemplateButtonPlugin /> - </> - ); -}; diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/SaveTemplateButton/index.ts b/packages/app-page-builder/src/templateEditor/config/editorBar/SaveTemplateButton/index.ts deleted file mode 100644 index c11d8545514..00000000000 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/SaveTemplateButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./SaveTemplateButton"; diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/TemplateSettings.tsx b/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/TemplateSettings.tsx deleted file mode 100644 index 5ce13e4b6f4..00000000000 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/TemplateSettings.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; -import { createDecorator } from "@webiny/app-admin"; -import { templateSettingsStateAtom } from "./state"; -import { useRecoilValue } from "recoil"; -import TemplateSettingsModal from "./TemplateSettingsModal"; - -/* For the time being, we're importing from the base editor, to not break things for existing users. */ -import { EditorBar } from "~/editor"; - -export const TemplateSettingsOverlay = createDecorator(EditorBar, EditorBar => { - return function TemplateSettingsOverlay() { - const isActive = useRecoilValue(templateSettingsStateAtom); - - return ( - <> - <EditorBar /> - {isActive ? <TemplateSettingsModal /> : null} - </> - ); - }; -}); diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/TemplateSettingsButton.tsx b/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/TemplateSettingsButton.tsx deleted file mode 100644 index 35239b8a069..00000000000 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/TemplateSettingsButton.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { useCallback } from "react"; -import { useRecoilState } from "recoil"; -import { IconButton } from "@webiny/ui/Button"; -import { createDecorator } from "@webiny/app-admin"; -import { ReactComponent as SettingsIcon } from "@material-design-icons/svg/round/settings.svg"; -import { templateSettingsStateAtom } from "./state"; -import { EditorBar } from "~/editor"; - -const TemplateSettingsButton = () => { - const [, setState] = useRecoilState(templateSettingsStateAtom); - const onClickHandler = useCallback(() => { - setState(true); - }, []); - - return <IconButton onClick={onClickHandler} icon={<SettingsIcon />} />; -}; - -export const AddTemplateSettingsButton = createDecorator(EditorBar.RightSection, RightSection => { - return function ComposeRightSection(props) { - return ( - <RightSection> - <TemplateSettingsButton /> - {props.children} - </RightSection> - ); - }; -}); diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/index.tsx b/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/index.tsx deleted file mode 100644 index acf1d76c647..00000000000 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import { TemplateSettingsOverlay } from "./TemplateSettings"; -import { AddTemplateSettingsButton } from "./TemplateSettingsButton"; - -export const TemplateSettingsPlugin = () => { - return ( - <> - <TemplateSettingsOverlay /> - <AddTemplateSettingsButton /> - </> - ); -}; diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/state.ts b/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/state.ts deleted file mode 100644 index 7c9c5698897..00000000000 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/TemplateSettings/state.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { atom } from "recoil"; - -export type TemplateSettingsState = boolean; - -export const templateSettingsStateAtom = atom<TemplateSettingsState>({ - key: "templateSettingsStateAtom", - default: false -}); diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/Title/index.ts b/packages/app-page-builder/src/templateEditor/config/editorBar/Title/index.ts deleted file mode 100644 index 2a7f3c58777..00000000000 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/Title/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./Title"; diff --git a/packages/app-page-builder/src/templateEditor/config/editorBar/index.ts b/packages/app-page-builder/src/templateEditor/config/editorBar/index.ts deleted file mode 100644 index b01e258dbc0..00000000000 --- a/packages/app-page-builder/src/templateEditor/config/editorBar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { EditorBarPlugins } from "./EditorBarPlugins"; diff --git a/packages/app-page-builder/src/templateEditor/editorConfig/TemplateEditorConfig.tsx b/packages/app-page-builder/src/templateEditor/editorConfig/TemplateEditorConfig.tsx new file mode 100644 index 00000000000..22e0c2b2865 --- /dev/null +++ b/packages/app-page-builder/src/templateEditor/editorConfig/TemplateEditorConfig.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { CompositionScope } from "@webiny/app-admin"; +import { EditorConfig } from "~/editor/config"; + +interface TemplateEditorConfigProps { + children: React.ReactNode; +} + +const BaseTemplateEditorConfig = ({ children }: TemplateEditorConfigProps) => { + return ( + <CompositionScope name={"pb.templateEditor"}> + <EditorConfig>{children}</EditorConfig> + </CompositionScope> + ); +}; + +export const TemplateEditorConfig = Object.assign(BaseTemplateEditorConfig, EditorConfig); diff --git a/packages/cwp-template-aws/template/common/apps/admin/src/plugins/pageBuilder/editorPlugins.ts b/packages/cwp-template-aws/template/common/apps/admin/src/plugins/pageBuilder/editorPlugins.ts index 36533f0d8f4..acf5cb2e75f 100644 --- a/packages/cwp-template-aws/template/common/apps/admin/src/plugins/pageBuilder/editorPlugins.ts +++ b/packages/cwp-template-aws/template/common/apps/admin/src/plugins/pageBuilder/editorPlugins.ts @@ -36,11 +36,6 @@ import codeGroup from "@webiny/app-page-builder/editor/plugins/elementGroups/cod import savedGroup from "@webiny/app-page-builder/editor/plugins/elementGroups/saved"; // Blocks import emptyBlock from "@webiny/app-page-builder/pageEditor/plugins/blocks/emptyBlock"; -// Toolbar -import addElement from "@webiny/app-page-builder/editor/plugins/toolbar/addElement"; -import navigator from "@webiny/app-page-builder/editor/plugins/toolbar/navigator"; -import saving from "@webiny/app-page-builder/editor/plugins/toolbar/saving"; -import { undo, redo } from "@webiny/app-page-builder/editor/plugins/toolbar/undoRedo"; // Element settings import animation from "@webiny/app-page-builder/editor/plugins/elementSettings/animation"; import deleteElement from "@webiny/app-page-builder/editor/plugins/elementSettings/delete"; @@ -115,12 +110,6 @@ export default [ socialGroup, codeGroup, savedGroup, - // Toolbar - addElement, - navigator(), - saving, - undo, - redo, // Element settings animation, background, diff --git a/packages/react-properties/src/Properties.tsx b/packages/react-properties/src/Properties.tsx index bf9e8f32c56..091c56f520c 100644 --- a/packages/react-properties/src/Properties.tsx +++ b/packages/react-properties/src/Properties.tsx @@ -38,7 +38,9 @@ function putPropertyBefore(properties: Property[], property: Property, before: s if (existingIndex > -1) { const existingProperty = properties[existingIndex]; const newProperties = properties.filter(p => p.id !== property.id); - const targetIndex = newProperties.findIndex(prop => prop.id === before); + const targetIndex = before.endsWith("$first") + ? 0 + : newProperties.findIndex(prop => prop.id === before); return [ ...newProperties.slice(0, targetIndex), existingProperty, @@ -56,7 +58,9 @@ function putPropertyAfter(properties: Property[], property: Property, after: str if (existingIndex > -1) { const [removedProperty] = properties.splice(existingIndex, 1); - const targetIndex = properties.findIndex(prop => prop.id === after); + const targetIndex = after.endsWith("$last") + ? properties.length - 1 + : properties.findIndex(prop => prop.id === after); return [ ...properties.slice(0, targetIndex + 1), removedProperty, diff --git a/webiny.project.ts b/webiny.project.ts index e9ef57d28da..e4f4f1e02e5 100644 --- a/webiny.project.ts +++ b/webiny.project.ts @@ -1,3 +1,5 @@ +/* We don't need the following rule in this file, as it's not being bundled with Webpack. */ +/* eslint-disable import/dynamic-import-chunkname */ export default { name: "webiny-js", cli: { @@ -11,30 +13,13 @@ export default { */ try { const modules = await Promise.allSettled([ - import( - /* webpackChunkName: "webinyCliPluginWorkspaces" */ - "@webiny/cli-plugin-workspaces" - ), - import( - /* webpackChunkName: "webinyCliPluginDeployPulumi" */ - "@webiny/cli-plugin-deploy-pulumi" - ), - import( - /* webpackChunkName: "webinyCwpTemplateAwsCli" */ - "@webiny/cwp-template-aws/cli" - ), - import( - /* webpackChunkName: "webinyCliPluginScaffold" */ "@webiny/cli-plugin-scaffold" - ), - import( - /* webpackChunkName: "webinyCliPluginScaffoldGraphQlService" */ "@webiny/cli-plugin-scaffold-graphql-service" - ), - import( - /* webpackChunkName: "webinyCliPluginScaffoldAdminAppModule" */ "@webiny/cli-plugin-scaffold-admin-app-module" - ), - import( - /* webpackChunkName: "webinyCliPluginScaffoldCi" */ "@webiny/cli-plugin-scaffold-ci" - ) + import("@webiny/cli-plugin-workspaces"), + import("@webiny/cli-plugin-deploy-pulumi"), + import("@webiny/cwp-template-aws/cli"), + import("@webiny/cli-plugin-scaffold"), + import("@webiny/cli-plugin-scaffold-graphql-service"), + import("@webiny/cli-plugin-scaffold-admin-app-module"), + import("@webiny/cli-plugin-scaffold-ci") ]); return modules