diff --git a/extensions/_wip/idp/MyIdpConfig.ts b/extensions/_wip/idp/MyIdpConfig.ts deleted file mode 100644 index cd57a45f2c1..00000000000 --- a/extensions/_wip/idp/MyIdpConfig.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Okta } from "webiny/idp/okta"; -import jwt from "jsonwebtoken"; -import type { Identity } from "@webiny/app-admin/src/domain/Identity.js"; -import type { Tenant } from "@webiny/api-core/src/types/tenancy.js"; - -interface IIdpConfig { - getIdentity(token: jwt.JwtPayload): Promise | Identity.Data; - getDefaultTenantId(identity: Identity.Data): string; - canAccessTenant(identity: Identity, tenant: Tenant): boolean; - verifyToken?(jwt: string): Promise | jwt.JwtPayload; - verifyTokenClaims?(jwt: jwt.JwtPayload): Promise | jwt.JwtPayload; -} - -class MyOktaConfig implements Okta.Api.Interface { - getIdentity(token: jwt.JwtPayload) { - return { - id: token["sub"], - type: "admin", - displayName: token["name"], - // User profile on this tenant - profile: { - roles: [token["role"]], - teams: [token["team"]], - firstName: token["given_name"], - lastName: token["family_name"], - email: token["email"] - }, - // For all other custom runtime data. This is not stored in the database. - context: { - clientId: token["iss"], - defaultTenant: token["tenantId"] - } - }; - } - - getDefaultTenantId(identity) { - // Return the default tenant ID for this identity. - return identity.context.defaultTenant; - } - - canAccessTenant(identity, tenant) { - // Implement logic that determines if the user can access the tenant. - return identity.context.tenantId === tenant.id; - } - - /** - * Optional - */ - async verifyToken(jwt: string): jwt.JwtPayload { - // If not using JWKeys, implement your own verification logic. - // The moment you implement this method, JWK verification is disabled. - } - - /** - * Optional - */ - async verifyTokenClaims(jwt: jwt.JwtPayload): jwt.JwtPayload { - // If using standard JWKeys, this callback will allow you to verify additional claims. - } -} - -export default Okta.Api.createImplementation({ - implementation: MyOktaConfig, - dependencies: [] -}); diff --git a/extensions/_wip/idp/MyIdpExtension.tsx b/extensions/_wip/idp/MyIdpExtension.tsx deleted file mode 100644 index 8bcec271140..00000000000 --- a/extensions/_wip/idp/MyIdpExtension.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import { Api } from "webiny/extensions"; - -export const MyIdpExtension = () => { - return ( - <> - - - ); -}; diff --git a/extensions/_wip/myOnEntryBeforeCreate.ts b/extensions/_wip/myOnEntryBeforeCreate.ts deleted file mode 100644 index c18ca0a8ec7..00000000000 --- a/extensions/_wip/myOnEntryBeforeCreate.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createImplementation } from "@webiny/di"; -import { OnEntryBeforeCreate } from "webiny/api/cms/features"; - -class MyOnEntryBeforeCreate implements OnEntryBeforeCreate.Interface { - execute({ model, entry }: OnEntryBeforeCreate.Params) { - // Do something. - } -} - -export default createImplementation({ - abstraction: OnEntryBeforeCreate, - implementation: MyOnEntryBeforeCreate, - dependencies: [GetCmsEntry, GetCmsModel] -}); diff --git a/packages/admin-ui/src/theme.css b/packages/admin-ui/src/theme.css index 9c38b3c6d93..5dfc231ab3c 100644 --- a/packages/admin-ui/src/theme.css +++ b/packages/admin-ui/src/theme.css @@ -346,6 +346,12 @@ @apply pr-xxl pl-xxl mx-auto w-full; } +@utility fill-grid { + background-image: radial-gradient(var(--color-neutral-strong) 1px, transparent 0px); + background-size: 15px 15px; + @apply bg-neutral-light; +} + @layer base { a { @apply text-accent-primary hover:underline focus-visible:outline-none focus-visible:ring-lg focus-visible:ring-primary-dimmed rounded-xs; diff --git a/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorConfig.tsx b/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorConfig.tsx index d77bbec8886..e3bdc468dcb 100644 --- a/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorConfig.tsx +++ b/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorConfig.tsx @@ -1,26 +1,15 @@ import React from "react"; import { PageEditorConfig as BaseConfig } from "@webiny/app-website-builder"; -import { PageEditorAutoSave } from "./PageEditorAutoSave.js"; -import { PageEditorSettings } from "./PageEditorSettings.js"; import { PageFormWorkflowStateTooltip } from "./PageFormWorkflowStateTooltip.js"; import { PageFormWorkflowStatePublishButton } from "./PageFormWorkflowStatePublishButton.js"; -import { PageEditorLayout } from "./PageEditorLayout.js"; -import { PageEditorToolbar } from "./PageEditorToolbar.js"; -import { PageEditorSidebar } from "./PageEditorSidebar.js"; +import { PageEditorTopBar } from "./PageEditorTopBar.js"; export const PageEditorConfig = () => { return ( <> - + {/* Toggle editor "readonly" mode, and add workflow alerts */} + - {/* Should remove autosave feature */} - - {/* Should remove settings button */} - - {/* Should remove left bar in the editor */} - - {/* Should remove right bar in the editor */} - {/* Should add a button with list of steps and their states + comment button in each row */} {/* should remove publish button from the form */} diff --git a/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorLayout.tsx b/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorLayout.tsx index c3fca299cea..06f33aa9335 100644 --- a/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorLayout.tsx +++ b/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorLayout.tsx @@ -1,11 +1,12 @@ -import React from "react"; +import React, { useEffect } from "react"; import { useSelectFromDocument } from "@webiny/app-website-builder/BaseEditor/hooks/useSelectFromDocument.js"; import { useApolloClient } from "@apollo/react-hooks"; import { useAuthentication } from "@webiny/app-admin"; -import { Components } from "@webiny/app-workflows"; +import { Components, useWorkflowState } from "@webiny/app-workflows"; import { WB_PAGE_APP } from "~/constants.js"; -import { PageFormWorkflowState } from "./PageFormWorkflowState.js"; import { PageEditorConfig } from "@webiny/app-website-builder"; +import { observer } from "mobx-react-lite"; +import { useDocumentEditor } from "@webiny/app-website-builder/DocumentEditor/index.js"; const { Ui } = PageEditorConfig; @@ -13,7 +14,22 @@ const { ContentReview: { WorkflowStateProvider } } = Components; -export const PageEditorLayout = Ui.TopBar.Layout.createDecorator(Original => { +const ToggleReadonly = observer(() => { + const { presenter } = useWorkflowState(); + const editor = useDocumentEditor(); + + const hasState = !!presenter.vm.state?.state; + + useEffect(() => { + editor.updateEditor(state => { + state.isReadOnly = hasState; + }); + }, [hasState]); + + return null; +}); + +export const PageEditorLayout = Ui.Layout.createDecorator(Original => { return function PageEditorTopBarWorkflowsState() { const page = useSelectFromDocument(doc => { return { @@ -33,10 +49,8 @@ export const PageEditorLayout = Ui.TopBar.Layout.createDecorator(Original => { client={client} title={`Website Builder: ${page.title}`} > - {/* Original top bar*/} - {/* Should render workflow state bar and the alert for storing changes */} - + ); }; diff --git a/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorSettings.tsx b/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorSettings.tsx deleted file mode 100644 index 2c219039028..00000000000 --- a/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorSettings.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; -import { PageEditorConfig } from "@webiny/app-website-builder"; -import { useWorkflowState } from "@webiny/app-workflows"; -import { observer } from "mobx-react-lite"; - -const { Ui } = PageEditorConfig; - -interface IWrappedSettings { - element?: React.ReactElement | null; -} - -const WrappedSettings = observer((props: IWrappedSettings) => { - const { presenter } = useWorkflowState(); - if (!presenter.vm.state?.state) { - return props.element; - } - - return null; -}); - -export const PageEditorSettings = Ui.TopBar.Action.createDecorator(Original => { - return function PageEditorSettingsDecorated(props) { - if (props.name === "buttonSettings") { - return } />; - } - return ; - }; -}); diff --git a/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorSidebar.tsx b/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorSidebar.tsx deleted file mode 100644 index 4f9e2f3087c..00000000000 --- a/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorSidebar.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import { EditorConfig } from "@webiny/app-website-builder/BaseEditor/index.js"; -import { useSelectFromDocument } from "@webiny/app-website-builder/BaseEditor/hooks/useSelectFromDocument.js"; - -export const PageEditorSidebar = EditorConfig.Ui.Sidebar.createDecorator(Original => { - return function PageEditorSidebarDecorated() { - const stepId = useSelectFromDocument((document): string | undefined => { - // @ts-expect-error - return document.workflows?.state?.stepId; - }); - if (stepId) { - return null; - } - return ; - }; -}); diff --git a/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorToolbar.tsx b/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorToolbar.tsx deleted file mode 100644 index 2aeb007ce4b..00000000000 --- a/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorToolbar.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { EditorConfig } from "@webiny/app-website-builder/BaseEditor/index.js"; -import React from "react"; -import { useSelectFromDocument } from "@webiny/app-website-builder/BaseEditor/hooks/useSelectFromDocument.js"; - -export const PageEditorToolbar = EditorConfig.Ui.Toolbar.createDecorator(Original => { - return function PageEditorToolbarDecorated() { - const stepId = useSelectFromDocument((document): string | undefined => { - // @ts-expect-error - return document.workflows?.state?.stepId; - }); - if (stepId) { - return null; - } - return ; - }; -}); diff --git a/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorTopBar.tsx b/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorTopBar.tsx new file mode 100644 index 00000000000..ad84600ff1c --- /dev/null +++ b/packages/app-website-builder-workflows/src/Components/PageEditor/PageEditorTopBar.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { useApolloClient } from "@apollo/react-hooks"; +import { PageEditorConfig } from "@webiny/app-website-builder"; +import { useSelectFromDocument } from "@webiny/app-website-builder/BaseEditor/hooks/useSelectFromDocument.js"; +import { useAuthentication } from "@webiny/app-admin"; +import { WorkflowStateProvider } from "@webiny/app-workflows/Components/WorkflowState/index.js"; +import { WB_PAGE_APP } from "~/constants.js"; +import { PageFormWorkflowState } from "~/Components/PageEditor/PageFormWorkflowState.js"; +import { ToggleEditorMode } from "~/Components/PageEditor/ToggleEditorMode.js"; + +const { Ui } = PageEditorConfig; + +export const PageEditorTopBar = Ui.TopBar.Layout.createDecorator(Original => { + return function PageEditorTopBarWorkflowsState() { + const page = useSelectFromDocument(doc => { + return { + id: doc.id, + title: doc.properties.title || "unknown page" + }; + }); + + const client = useApolloClient(); + const { identity } = useAuthentication(); + + return ( + + + + + + ); + }; +}); diff --git a/packages/app-website-builder-workflows/src/Components/PageEditor/PageFormWorkflowState.tsx b/packages/app-website-builder-workflows/src/Components/PageEditor/PageFormWorkflowState.tsx index 61f23d66a61..45a85d9923c 100644 --- a/packages/app-website-builder-workflows/src/Components/PageEditor/PageFormWorkflowState.tsx +++ b/packages/app-website-builder-workflows/src/Components/PageEditor/PageFormWorkflowState.tsx @@ -1,6 +1,5 @@ import React from "react"; import { Components } from "@webiny/app-workflows"; -import { Alert } from "@webiny/admin-ui"; const { ContentReview: { WorkflowStateBar } @@ -9,15 +8,15 @@ const { export const PageFormWorkflowState = () => { return ( - {({ state, stateBar }) => { + {({ stateBar }) => { return ( -
+
{stateBar} - {state ? ( + {/*{state ? ( Any changes you do on the page will not be stored! - ) : null} + ) : null}*/}
); }} diff --git a/packages/app-website-builder-workflows/src/Components/PageEditor/ToggleEditorMode.tsx b/packages/app-website-builder-workflows/src/Components/PageEditor/ToggleEditorMode.tsx new file mode 100644 index 00000000000..c273d9a3427 --- /dev/null +++ b/packages/app-website-builder-workflows/src/Components/PageEditor/ToggleEditorMode.tsx @@ -0,0 +1,19 @@ +import { useEffect } from "react"; +import { observer } from "mobx-react-lite"; +import { useWorkflowState } from "@webiny/app-workflows"; +import { useDocumentEditor } from "@webiny/app-website-builder/DocumentEditor/index.js"; + +export const ToggleEditorMode = observer(() => { + const { presenter } = useWorkflowState(); + const editor = useDocumentEditor(); + + const hasState = !!presenter.vm.state?.state; + + useEffect(() => { + editor.updateEditor(state => { + state.isReadOnly = hasState; + }); + }, [hasState]); + + return null; +}); diff --git a/packages/app-website-builder/src/BaseEditor/config/EditorConfig.tsx b/packages/app-website-builder/src/BaseEditor/config/EditorConfig.tsx index c5e46a58cce..e5e437c1ab3 100644 --- a/packages/app-website-builder/src/BaseEditor/config/EditorConfig.tsx +++ b/packages/app-website-builder/src/BaseEditor/config/EditorConfig.tsx @@ -13,6 +13,8 @@ import { ElementProperties, ElementProperty } from "./ElementProperty.js"; import { ElementAction, ElementActions } from "./ElementAction.js"; import type { ElementInputConfig } from "./ElementInput.js"; import { ElementInput } from "./ElementInput.js"; +import { IsNotReadOnly } from "~/BaseEditor/config/IsNotReadOnly.js"; +import { IsReadOnly } from "~/BaseEditor/config/IsReadOnly.js"; interface EditorConfig { elements: ElementConfig[]; @@ -21,7 +23,7 @@ interface EditorConfig { const base = createConfigurableComponent("DocumentEditor"); -export const EditorConfig = Object.assign(base.Config, { +export const EditorConfigComponents = { /** * Components to configure editor UI. */ @@ -33,6 +35,8 @@ export const EditorConfig = Object.assign(base.Config, { TopBar, Toolbar, Sidebar, + IsReadOnly, + IsNotReadOnly, OnActiveElement, NoActiveElement }, @@ -57,7 +61,9 @@ export const EditorConfig = Object.assign(base.Config, { * Access full editor config. WARNING: very low-level, we don't recommend using this directly! */ useEditorConfig -}); +}; + +export const EditorConfig = Object.assign(base.Config, EditorConfigComponents); export const EditorWithConfig = Object.assign(base.WithConfig, { displayName: "EditorWithConfig" }); diff --git a/packages/app-website-builder/src/BaseEditor/config/IsNotReadOnly.tsx b/packages/app-website-builder/src/BaseEditor/config/IsNotReadOnly.tsx new file mode 100644 index 00000000000..8b79574db72 --- /dev/null +++ b/packages/app-website-builder/src/BaseEditor/config/IsNotReadOnly.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { useSelectFromEditor } from "~/BaseEditor/hooks/useSelectFromEditor.js"; + +export interface IsNotReadOnlyProps { + children: React.ReactNode; +} + +export const IsNotReadOnly = ({ children }: IsNotReadOnlyProps) => { + const isEditorReadOnly = useSelectFromEditor(state => state.isReadOnly); + + if (isEditorReadOnly) { + return null; + } + + return <>{children}; +}; diff --git a/packages/app-website-builder/src/BaseEditor/config/IsReadOnly.tsx b/packages/app-website-builder/src/BaseEditor/config/IsReadOnly.tsx new file mode 100644 index 00000000000..3091fbcf030 --- /dev/null +++ b/packages/app-website-builder/src/BaseEditor/config/IsReadOnly.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { useSelectFromEditor } from "~/BaseEditor/hooks/useSelectFromEditor.js"; + +export interface IsNotReadOnlyProps { + children: React.ReactNode; +} + +export const IsReadOnly = ({ children }: IsNotReadOnlyProps) => { + const isEditorReadOnly = useSelectFromEditor(state => state.isReadOnly); + + if (isEditorReadOnly) { + return null; + } + + return <>{children}; +}; diff --git a/packages/app-website-builder/src/BaseEditor/config/Layout.tsx b/packages/app-website-builder/src/BaseEditor/config/Layout.tsx index 7a80a396942..cd582d8370e 100644 --- a/packages/app-website-builder/src/BaseEditor/config/Layout.tsx +++ b/packages/app-website-builder/src/BaseEditor/config/Layout.tsx @@ -2,6 +2,9 @@ import React, { useEffect } from "react"; import { makeDecoratable } from "@webiny/react-composition"; import { EditorConfig } from "./EditorConfig.js"; import styled from "@emotion/styled"; +import { IsNotReadOnly } from "~/BaseEditor/config/IsNotReadOnly.js"; +import { useReservedUISpace } from "~/BaseEditor/hooks/useReservedUISpace.js"; +import { useDocumentEditor } from "~/DocumentEditor/index.js"; const EditorLayoutContainer = styled.div` height: 100%; @@ -9,6 +12,14 @@ const EditorLayoutContainer = styled.div` `; export const Layout = makeDecoratable("EditorLayout", () => { + const editor = useDocumentEditor(); + + useReservedUISpace(dimensions => { + editor.updateEditor(state => { + state.uiReservedSpace = dimensions; + }); + }); + useEffect(() => { const currentOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; @@ -20,12 +31,16 @@ export const Layout = makeDecoratable("EditorLayout", () => { return ( -
- +
+ + +
- + + +
diff --git a/packages/app-website-builder/src/BaseEditor/config/Sidebar/Layout.tsx b/packages/app-website-builder/src/BaseEditor/config/Sidebar/Layout.tsx index 4ab2944cac9..b5893112551 100644 --- a/packages/app-website-builder/src/BaseEditor/config/Sidebar/Layout.tsx +++ b/packages/app-website-builder/src/BaseEditor/config/Sidebar/Layout.tsx @@ -5,7 +5,11 @@ import { Sidebar } from "./Sidebar.js"; export const Layout = makeDecoratable("SidebarLayout", () => { return ( -
+
{ return (
{ return ( -
+
); diff --git a/packages/app-website-builder/src/BaseEditor/config/TopBar/Layout.tsx b/packages/app-website-builder/src/BaseEditor/config/TopBar/Layout.tsx index 13676f63811..24b605cf61b 100644 --- a/packages/app-website-builder/src/BaseEditor/config/TopBar/Layout.tsx +++ b/packages/app-website-builder/src/BaseEditor/config/TopBar/Layout.tsx @@ -7,6 +7,7 @@ export const Layout = makeDecoratable("TopBarLayout", () => { return ( } middle={} end={ diff --git a/packages/app-website-builder/src/BaseEditor/defaultConfig/Content/Breadcrumbs/Breadcrumbs.tsx b/packages/app-website-builder/src/BaseEditor/defaultConfig/Content/Breadcrumbs/Breadcrumbs.tsx index 6fb6e0941db..224bd67a269 100644 --- a/packages/app-website-builder/src/BaseEditor/defaultConfig/Content/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/app-website-builder/src/BaseEditor/defaultConfig/Content/Breadcrumbs/Breadcrumbs.tsx @@ -51,7 +51,12 @@ export const Breadcrumbs = () => { }, []); return ( -
    +
      {items.map(({ id, label }, index) => (
    • { }, [path, previewDomain]); return ( -
      +
      { const iframeRef = useRef(null); - const previewSize = useResponsiveContainer(props.viewportManager); + const previewBodyRef = useRef(null); + const previewWidth = useResponsiveContainer(props.viewportManager); const { viewport } = usePreviewData(); + const editor = useSelectFromEditor(state => ({ + isReadOnly: state.isReadOnly, + reservedHeight: state.uiReservedSpace.height + })); + + const isReadOnly = editor.isReadOnly; + + // When preview width changes, we need to force calculation of the iframe scrollHeight + // by reducing the height of its parent div to the current viewport height. + // Otherwise, the iframe grows to fill the parent div, and will not report the correct content height. + useEffect(() => { + if (previewBodyRef.current) { + const minHeight = `calc(100vh - ${editor.reservedHeight}px)`; + previewBodyRef.current.style.minHeight = minHeight; + } + }, [previewWidth]); const iframeUrl = useMemo(() => { const localUrl = new URL(url); @@ -28,15 +47,7 @@ export const Iframe = observer(({ url, timestamp, ...props }: IframeProps) => { }, [url, timestamp]); return ( -
      + {props.showLoading ? ( { {/* Content wrapper - sized by iframe content */}
      - + {!isReadOnly ? : null}